diff --git a/app/SideBar.qml b/app/SideBar.qml index 9a8473890c14929f24761b04765d31710e7da072..568bff8381fa723c71e08ffc1001c286c3062308 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -69,6 +69,9 @@ Item{ if(model.type === QArgType.Text){ //文字输入框 return textComponent } + if(model.type === QArgType.Bool){ //布尔值 + return boolComponent + } } } } @@ -122,6 +125,7 @@ Item{ comboModel.append({"text":items[i]}) } } + root.parameters[index] = value = parameterComboBox.currentIndex } } } @@ -276,4 +280,32 @@ Item{ } } } + Component{ + id: boolComponent + RowLayout{ + spacing: 5 + width: parameterList.width + + Text{ + id:nametext + text: model.name + } + Rectangle{ + Layout.fillHeight: true + Layout.preferredWidth: 1 + color: "black" + } + CheckBox{ + id: parameterCheckBox + + Component.onCompleted: { + checked = (model.content === "true") + root.parameters[index] = checked + } + onCheckedChanged: { + root.parameters[index] = checked + } + } + } + } } diff --git a/model/data/CMakeLists.txt b/model/data/CMakeLists.txt index bb0e2e83569f17296de878bec0ac0bf97d35ad14..f56b33298e84669a8078a294f18404fc29b331a1 100644 --- a/model/data/CMakeLists.txt +++ b/model/data/CMakeLists.txt @@ -27,4 +27,4 @@ else() ) endif() -add_subdirectory(test) \ No newline at end of file +add_subdirectory(test) diff --git a/model/data/GeometryData.h b/model/data/GeometryData.h index bf503c9cd571a0726c755fcf35c21da326b532e1..183f0f4fa74ea13e8b98e57177f3fb8e402c8483 100644 --- a/model/data/GeometryData.h +++ b/model/data/GeometryData.h @@ -9,7 +9,7 @@ struct GeometryDataVtk; // 以后需要控制点 / 曲率等,可继续添加字段 struct GeometryData { GeometryData(); - ~GeometryData(); + ~GeometryData(); std::unique_ptr rootShape; // 读取 STEP/IGES 后的拓扑根 GeometrySubshapeIndex index; void ensureIndexBuilt(GeometryRegistry& reg); diff --git a/model/data/ModelLayer.h b/model/data/ModelLayer.h index b89ce9beeb1049c5a0ec85311dc0f5fe9b55b118..e134208494b15fa5f23cbfe320964a59c8fbdddc 100644 --- a/model/data/ModelLayer.h +++ b/model/data/ModelLayer.h @@ -80,6 +80,9 @@ public: MeshIDMap& edgeIdMap(); const MeshIDMap& edgeIdMap() const; + // 将运行期新生成的点追加到全局点池,返回这批点的第一个全局点 ID。 + Index appendGlobalPoints(const std::vector>& pts); + private: Index allocateComponentId() noexcept; @@ -91,7 +94,6 @@ private: Index max_index_{ -1 }; //!< 最大索引值,用于唯一标识模型 Index next_component_id_ { 0 }; //!< component_id 全局发号器(只增不减) - Index appendGlobalPoints(const std::vector>& pts); std::vector> global_points_; MeshIDMap edge_id_map_; // 先只维护 edge 的 global->local diff --git a/plugins/algo/CMakeLists.txt b/plugins/algo/CMakeLists.txt index 1da5255d65203b7069e57f9abc5bfdc9a66b486a..55c4c27472cde43347d98d0a22009b0dfaf3237a 100644 --- a/plugins/algo/CMakeLists.txt +++ b/plugins/algo/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(CmdExecutePlugin) -add_subdirectory(TetGenPlugin) \ No newline at end of file +add_subdirectory(TetGenPlugin) +add_subdirectory(GmshPlugin) diff --git a/plugins/algo/GmshPlugin/CMakeLists.txt b/plugins/algo/GmshPlugin/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b2db17ee2eda72cde61839bfba5179353f1de16f --- /dev/null +++ b/plugins/algo/GmshPlugin/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(gmsh REQUIRED) + +precess_add_algo_plugin(GmshPlugin + SOURCES + "GmshMeshHandler.cpp" + "IncrementalMeshTools.cpp" + PLUGIN_H "GmshMeshPlugin.h" +) + +precess_plugin_link_libraries(GmshPlugin + gmsh::shared + TKBRep # BRep_Builder, BRepTools + TKernel # Standard_* 基础类型 + TKG3d # gp_* 准则基础 + TKTopAlgo # TopExp_Explorer +) + +add_subdirectory(test) diff --git a/plugins/algo/GmshPlugin/GmshIncrementalMeshState.h b/plugins/algo/GmshPlugin/GmshIncrementalMeshState.h new file mode 100644 index 0000000000000000000000000000000000000000..3166da6f4326dfd6faafec89896a3b6d257a2dc0 --- /dev/null +++ b/plugins/algo/GmshPlugin/GmshIncrementalMeshState.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Core.h" + +#include +#include +#include + +// 保存一条已生成网格的 CAD 边,供相邻面复用边界节点。 +struct MeshedEdgeData { + std::vector coords; + std::vector paramCoords; +}; + +// 保存单个 CAD 面的网格结果,面删除和重划分时用它重建整体 MeshData。 +struct SingleFaceMeshResult { + std::vector> vertices; + std::vector face_vertices; + std::vector face_vertices_offset; + // 写入 MeshData 后对应的全局点 ID,用于只删除本次 Gmsh 生成的单元。 + std::vector global_face_vertices; + bool success { false }; +}; + +// 保存一个 component 的 Gmsh 增量网格状态,由 Gmsh 插件管理生命周期。 +struct GmshIncrementalMeshState { + // 已划分 CAD 边的节点缓存,key 使用 geometry 的全局 CAD 边 ID。 + std::map meshedEdgesCache; + // 已划分 CAD 面的网格结果,key 使用 geometry 的全局 CAD 面 ID。 + std::map meshedFacesCache; + // 已划分 CAD 边被多少个面复用,用于删除面网格时判断是否清理边缓存。 + std::map meshedEdgeRefCounts; +}; diff --git a/plugins/algo/GmshPlugin/GmshMeshHandler.cpp b/plugins/algo/GmshPlugin/GmshMeshHandler.cpp new file mode 100644 index 0000000000000000000000000000000000000000..24b06f1a8d2ea84bce699c41e3f09b25e6e2ef5a --- /dev/null +++ b/plugins/algo/GmshPlugin/GmshMeshHandler.cpp @@ -0,0 +1,298 @@ +#include "GmshMeshHandler.h" + +#include "ArgObject.h" +#include "ComponentData.h" +#include "ComponentOperator.h" +#include "GmshMeshOptions.h" +#include "GeometryData.h" +#include "GeometryRegistry.h" +#include "GeometrySubshapeIndex.h" +#include "IncrementalMeshTools.h" +#include "MeshData.h" +#include "ModelIOSystemBase.h" +#include "ModelLayer.h" +#include "Selection.h" +#include "TempFile.h" + +#include + +#include +#include +#include +#include +#include + +using core::ArgType; + +namespace { + +// 从 Text 参数读取可选 double;空字符串表示用户不设置,后续使用默认值。 +std::optional readOptionalDouble(const std::vector& args, std::size_t index) +{ + if (args.size() <= index) + return std::nullopt; + + const std::string* text = args[index].get(); + if (!text || text->empty()) + return std::nullopt; + + try { + std::size_t parsed = 0; + double value = std::stod(*text, &parsed); + if (parsed != text->size() || !std::isfinite(value)) + throw std::invalid_argument("not a finite double"); + return value; + } catch (...) { + spdlog::warn("GmshMesh: invalid double parameter {} = '{}'", index, *text); + return std::nullopt; + } +} + +// 从 Text 参数读取可选 int;空字符串表示用户不设置。 +std::optional readOptionalInt(const std::vector& args, std::size_t index) +{ + if (args.size() <= index) + return std::nullopt; + + const std::string* text = args[index].get(); + if (!text || text->empty()) + return std::nullopt; + + try { + std::size_t parsed = 0; + int value = std::stoi(*text, &parsed); + if (parsed != text->size()) + throw std::invalid_argument("not an integer"); + return value; + } catch (...) { + spdlog::warn("GmshMesh: invalid int parameter {} = '{}'", index, *text); + return std::nullopt; + } +} + +// Combo 参数没有“空值”,所以第一个选项统一作为“默认”。 +int readComboIndex(const std::vector& args, std::size_t index, int defaultValue = 0) +{ + if (args.size() <= index) + return defaultValue; + + const int* value = args[index].get(); + return value ? *value : defaultValue; +} + +// 从 Bool 参数读取开关; +bool readBoolArg(const std::vector& args, std::size_t index, bool defaultValue) +{ + if (args.size() <= index) + return defaultValue; + + const bool* value = args[index].get(); + return value ? *value : defaultValue; +} + +// 校验传入 Gmsh 前的数值参数,避免把不合法尺寸或质量值传递给底层库。 +bool validateMeshingParameters(const IncrementalMeshTools::GmshMeshParameters& parameters) +{ + if (parameters.targetMeshSize < 0.0 + || parameters.minMeshSize < 0.0 + || parameters.maxMeshSize < 0.0 + || parameters.smoothingSteps < 0 + || parameters.structuredEdgeDivisions < 0 + || parameters.quadMinQuality < 0.0 + || parameters.quadMinQuality > 1.0) { + spdlog::error("GmshMesh: invalid meshing parameter range"); + return false; + } + + if (parameters.minMeshSize > 0.0 + && parameters.maxMeshSize > 0.0 + && parameters.minMeshSize > parameters.maxMeshSize) { + spdlog::error("GmshMesh: minimum mesh size must not exceed maximum mesh size"); + return false; + } + return true; +} + +} // namespace + +std::any systems::algo::GmshMeshHandler::execute( + HandlerContext& context, + const std::vector& args) +{ + int operationMode = 1; + IncrementalMeshTools::GmshMeshParameters parameters; + + if (args.size() <= 4) { + // 兼容旧参数顺序:选择面、网格尺寸、操作模式、网格类型。 + parameters.targetMeshSize = readOptionalDouble(args, 1).value_or(0.0); + if (args.size() >= 3) { + const std::string* opStr = args[2].get(); + if (opStr && !opStr->empty()) { + try { + operationMode = std::stoi(*opStr); + } catch (...) { + spdlog::warn("GmshMesh: invalid operation mode '{}', using default 1 (Mesh)", *opStr); + } + } + } + if (args.size() >= 4) { + const int* value = args[3].get(); + if (value) + parameters.meshTypeIndex = *value; + } + } else { + operationMode = readComboIndex(args, 1) + 1; + parameters.targetMeshSize = readOptionalDouble(args, 2).value_or(0.0); + parameters.minMeshSize = readOptionalDouble(args, 3).value_or(0.0); + parameters.maxMeshSize = readOptionalDouble(args, 4).value_or(0.0); + parameters.meshAlgorithm = gmshComboValue( + kGmshMeshAlgorithmComboValues, readComboIndex(args, 5)); + parameters.meshTypeIndex = readComboIndex(args, 6); + parameters.algorithmSwitchOnFailure = 0; + parameters.smoothingSteps = readOptionalInt(args, 7).value_or(0); + parameters.recombineAlgorithm = gmshComboValue( + kGmshRecombinationAlgorithmComboValues, readComboIndex(args, 8)); + parameters.quadMinQuality = readOptionalDouble(args, 9).value_or(0.0); + parameters.structuredEdgeDivisions = readOptionalInt(args, 10).value_or(0); + } + bool writeModel = readBoolArg(args, 11, true); + + if (!validateMeshingParameters(parameters)) + return {}; + + ComponentData& comp = context.cur_component.component(); + ModelLayer& modelLayer = context.cur_component.manager(); + removeExpiredStates(modelLayer); + + GeometryData* geometry = comp.geometry.get(); + if (!geometry || !geometry->rootShape) { + spdlog::error("GmshMesh: current component {} has no geometry", + context.cur_component.componentId()); + return {}; + } + + geometry->ensureIndexBuilt(modelLayer.geomRegistry()); + const auto* selection = args.empty() + ? nullptr + : args[0].get(); + if (!selection || !*selection + || (*selection)->type != ElementEnum::GeometryFace + || (*selection)->ids.empty()) { + spdlog::error("GmshMesh: select at least one geometry face"); + return {}; + } + + for (GeomFaceId faceId : (*selection)->ids) { + const TopoDS_Shape* selectedFace = modelLayer.geomRegistry().getFace(faceId); + const int localFaceId = selectedFace + ? geometry->index.type_maps[ + GeometrySubshapeIndex::typeIndex(TopAbs_FACE)].FindIndex(*selectedFace) + : 0; + if (localFaceId <= 0) { + spdlog::error("GmshMesh: selected geometry face {} is not in current component", faceId); + return {}; + } + } + + // 获取或创建当前 component 的 MeshData,Gmsh 生成结果直接写回该 component。 + MeshData* meshData = comp.mesh.get(); + if (!meshData) { + spdlog::info("GmshMesh: mesh_data is null, create a new one"); + comp.mesh = std::make_unique(); + comp.mesh->init(); + meshData = comp.mesh.get(); + } + + GmshIncrementalMeshState& state = component_states_[context.cur_component.componentId()]; + + if (parameters.targetMeshSize <= 0.0) + parameters.targetMeshSize = IncrementalMeshTools::estimateMeshSize(*geometry); + + if (operationMode < 1 || operationMode > 3) { + spdlog::warn("GmshMesh: unknown operation mode {}, skip", operationMode); + return {}; + } + + std::size_t successCount = 0; + std::size_t failedCount = 0; + for (GeomFaceId faceId : (*selection)->ids) { + if (operationMode == 1) { + if (state.meshedFacesCache.find(faceId) != state.meshedFacesCache.end()) { + spdlog::info("GmshMesh: face {} already meshed, skip mesh mode", faceId); + continue; + } + + auto result = IncrementalMeshTools::meshSingleFace( + *meshData, *geometry, state, modelLayer, + faceId, parameters.targetMeshSize, parameters); + if (!result.success) { + spdlog::warn("GmshMesh: face {} meshing failed", faceId); + ++failedCount; + continue; + } + } else if (operationMode == 2) { + if (!IncrementalMeshTools::deleteFaceMesh( + *meshData, *geometry, state, modelLayer, faceId)) { + spdlog::warn("GmshMesh: delete face {} failed", faceId); + ++failedCount; + continue; + } + } else { + auto result = IncrementalMeshTools::remeshSingleFace( + *meshData, *geometry, state, modelLayer, + faceId, parameters.targetMeshSize, parameters); + if (!result.success) { + spdlog::warn("GmshMesh: face {} remeshing failed", faceId); + ++failedCount; + continue; + } + } + ++successCount; + } + + spdlog::info("GmshMesh: {} selected faces processed: {} succeeded, {} failed", + (*selection)->ids.size(), successCount, failedCount); + + if (writeModel && successCount > 0) { + std::string meshOut = core::TempFile::instance().path().string() + "_total_mesh.obj"; + context.io_system.writeComponents( + { context.cur_component.componentId() }, + meshOut, + "Wavefront .obj file", + {}); + context.io_system.read(meshOut, "Wavefront .obj file", {}); + } + if (successCount > 0) + context.cur_component.notifyChanged(); + + return {}; +} + +void systems::algo::GmshMeshHandler::removeExpiredStates(const ModelLayer& model_layer) +{ + for (auto it = component_states_.begin(); it != component_states_.end();) { + if (!model_layer.findComponent(it->first)) { + it = component_states_.erase(it); + } else { + ++it; + } + } +} + +std::vector systems::algo::GmshMeshHandler::args_type() const +{ + return { + ArgType { ArgTypeEnum::Selector, "选择几何面", "" }, + ArgType { ArgTypeEnum::Combo, "操作模式", "划分,删除,重划分" }, + ArgType { ArgTypeEnum::Text, "目标网格尺寸(留空自动)", "" }, + ArgType { ArgTypeEnum::Text, "最小网格尺寸(留空默认)", "" }, + ArgType { ArgTypeEnum::Text, "最大网格尺寸(留空默认)", "" }, + ArgType { ArgTypeEnum::Combo, "二维网格算法", kGmshMeshAlgorithmComboText }, + ArgType { ArgTypeEnum::Combo, "网格类型", kGmshSurfaceMeshTypeComboText }, + ArgType { ArgTypeEnum::Text, "平滑次数(留空默认)", "" }, + ArgType { ArgTypeEnum::Combo, "四边形重组算法", kGmshRecombinationAlgorithmComboText }, + ArgType { ArgTypeEnum::Text, "四边形最低质量(0~1,留空默认)", "" }, + ArgType { ArgTypeEnum::Text, "结构化网格边划分数(留空自动)", "" }, + ArgType { ArgTypeEnum::Bool, "是否写出网格", "true" } + }; +} diff --git a/plugins/algo/GmshPlugin/GmshMeshHandler.h b/plugins/algo/GmshPlugin/GmshMeshHandler.h new file mode 100644 index 0000000000000000000000000000000000000000..61cfde182f9a0a4f7e1af858e559319da953328a --- /dev/null +++ b/plugins/algo/GmshPlugin/GmshMeshHandler.h @@ -0,0 +1,25 @@ +#pragma once +#include "AlgorithmHandler.h" +#include "GmshIncrementalMeshState.h" +#include +#include +#include + +class ModelLayer; + +namespace systems::algo { +class GmshMeshHandler : public AlgorithmHandler { +public: + GmshMeshHandler() = default; + ~GmshMeshHandler() override = default; + + std::any execute(HandlerContext& context, const std::vector& args) override; + std::vector args_type() const override; + +private: + // 删除已经不存在的 component 对应缓存,避免插件长期运行时残留无效状态。 + void removeExpiredStates(const ModelLayer& model_layer); + + std::unordered_map component_states_; +}; +} // namespace systems::algo diff --git a/plugins/algo/GmshPlugin/GmshMeshOptions.h b/plugins/algo/GmshPlugin/GmshMeshOptions.h new file mode 100644 index 0000000000000000000000000000000000000000..54c7e4e1de55bbc6477f0895c456c22e847e691b --- /dev/null +++ b/plugins/algo/GmshPlugin/GmshMeshOptions.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +// Gmsh 曲面网格类型。数值需要和前端Combo 的选项顺序一致。 +enum class GmshSurfaceMeshType { + // 0:普通三角形网格,不启用四边形重组或结构化约束。 + Triangle = 0, + // 1:四边形主导网格,先生成二维网格,再通过 Recombine 尽量重组成四边形。 + QuadDominant = 1, + // 2:结构化四边形网格,使用 Transfinite Curve/Surface 约束生成行列结构。 + StructuredQuadrilateral = 2 +}; + +// Gmsh 二维曲面网格算法,对应 Mesh.Algorithm 的官方取值。 +enum class GmshMeshAlgorithm { + // 1:MeshAdapt。基于局部网格修改,复杂曲面上通常更鲁棒。 + MeshAdapt = 1, + // 2:Automatic。Gmsh 自动选择;平面通常用 Delaunay,非平面通常用 MeshAdapt。 + Automatic = 2, + // 3:Initial mesh only。只生成初始网格,主要用于调试,暂不暴露到 UI。 + InitialMeshOnly = 3, + // 5:Delaunay。平面大网格速度较快,对复杂尺寸场也比较适合。 + Delaunay = 5, + // 6:Frontal-Delaunay。Gmsh 二维算法默认值,通常能得到较好的三角形质量。 + FrontalDelaunay = 6, + // 7:BAMG。偏各向异性三角网格;当前插件没有专门的各向异性参数。 + Bamg = 7, + // 8:Frontal-Delaunay for Quads。生成更适合后续四边形重组的三角网格。 + FrontalDelaunayForQuads = 8, + // 9:Packing of Parallelograms。四边形相关算法,适用范围比默认算法更窄。 + PackingParallelograms = 9, + // 11:Quasi-structured Quad。准结构化四边形算法;暂不暴露到 UI,避免和结构化四边形混淆。 + QuasiStructuredQuad = 11 +}; + +// Gmsh 四边形重组算法,对应 Mesh.RecombinationAlgorithm 的真实取值。 +enum class GmshRecombinationAlgorithm { + // 0:Simple。简单重组算法。 + Simple = 0, + // 1:Blossom。Gmsh 默认重组算法,通常质量和成功率更均衡。 + Blossom = 1, + // 2:Simple full-quad。尝试生成全四边形网格。 + SimpleFullQuad = 2, + // 3:Blossom full-quad。基于 Blossom 的全四边形重组。 + BlossomFullQuad = 3 +}; + +// 前端 Combo 文案和下面的取值数组必须保持同一顺序。 +inline constexpr const char* kGmshSurfaceMeshTypeComboText = + "三角形,四边形主导,结构化四边形"; + +inline constexpr const char* kGmshMeshAlgorithmComboText = + "默认(Frontal-Delaunay),MeshAdapt,Automatic,Delaunay," + "Frontal-Delaunay,BAMG,Frontal-Delaunay for Quads," + "Packing Parallelograms"; + +inline constexpr std::array kGmshMeshAlgorithmComboValues { + static_cast(GmshMeshAlgorithm::FrontalDelaunay), + static_cast(GmshMeshAlgorithm::MeshAdapt), + static_cast(GmshMeshAlgorithm::Automatic), + static_cast(GmshMeshAlgorithm::Delaunay), + static_cast(GmshMeshAlgorithm::FrontalDelaunay), + static_cast(GmshMeshAlgorithm::Bamg), + static_cast(GmshMeshAlgorithm::FrontalDelaunayForQuads), + static_cast(GmshMeshAlgorithm::PackingParallelograms) +}; + +inline constexpr const char* kGmshRecombinationAlgorithmComboText = + "默认(Blossom),Simple,Blossom,Simple full-quad,Blossom full-quad"; + +inline constexpr std::array kGmshRecombinationAlgorithmComboValues { + static_cast(GmshRecombinationAlgorithm::Blossom), + static_cast(GmshRecombinationAlgorithm::Simple), + static_cast(GmshRecombinationAlgorithm::Blossom), + static_cast(GmshRecombinationAlgorithm::SimpleFullQuad), + static_cast(GmshRecombinationAlgorithm::BlossomFullQuad) +}; + +// 将 Combo 索引转换为 Gmsh option 的真实取值;索引异常时使用第 0 项默认值。 +template +int gmshComboValue(const std::array& values, int comboIndex) +{ + if (comboIndex < 0) + return values[0]; + + std::size_t index = static_cast(comboIndex); + if (index >= values.size()) + return values[0]; + + return values[index]; +} diff --git a/plugins/algo/GmshPlugin/GmshMeshPlugin.h b/plugins/algo/GmshPlugin/GmshMeshPlugin.h new file mode 100644 index 0000000000000000000000000000000000000000..38dea1c6bfeaed676667a5f680d03e3165537b18 --- /dev/null +++ b/plugins/algo/GmshPlugin/GmshMeshPlugin.h @@ -0,0 +1,19 @@ +#pragma once +#include "PluginBase.h" +#include "GmshMeshHandler.h" +#include "HandlerCreatorDestroyerFactory.h" +#include + +namespace systems::algo { +class GmshMeshPlugin : public QObject, public PluginBase { + Q_OBJECT + Q_INTERFACES(systems::PluginBase) + Q_PLUGIN_METADATA(IID "com.PreCess.systems.algo.GmshMeshPlugin/1.0" + FILE "GmshPlugin.json") +private: + const HandlerCreatorDestroyer& getHandlerCreatorDestroyer() noexcept override final + { + return HandlerCreatorDestroyerFactory::get(); + } +}; +} // namespace systems::algo \ No newline at end of file diff --git a/plugins/algo/GmshPlugin/GmshPlugin.json b/plugins/algo/GmshPlugin/GmshPlugin.json new file mode 100644 index 0000000000000000000000000000000000000000..3323ba70183547c3c9ead4795757f4fc632e595d --- /dev/null +++ b/plugins/algo/GmshPlugin/GmshPlugin.json @@ -0,0 +1,7 @@ +{ + "system": "AlgorithmSystem", + "handler": { + "name": "GmshPlugin", + "display_name": "Gmsh渐进式划分网格" + } +} \ No newline at end of file diff --git a/plugins/algo/GmshPlugin/IncrementalMeshTools.cpp b/plugins/algo/GmshPlugin/IncrementalMeshTools.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1cf5a621477e1a5dd85a81932028437f2fd0dab4 --- /dev/null +++ b/plugins/algo/GmshPlugin/IncrementalMeshTools.cpp @@ -0,0 +1,902 @@ +#include "IncrementalMeshTools.h" + +#include "GeometryRegistry.h" +#include "GmshMeshOptions.h" +#include "MeshData.h" +#include "ModelLayer.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// 合并网格时的顶点去重 +class TempNodeLookup { +public: + explicit TempNodeLookup( + MeshData& mesh_data, + ModelLayer& model_layer, + double tolerance = 1e-7) + : _mesh_data(mesh_data) + , _model_layer(model_layer) + , _tolerance(tolerance) + { + const auto& vertices = _mesh_data.vertex_positions_; + const auto& global_ids = _mesh_data.local_to_global_; + for (size_t i = 0; i < vertices.size() && i < global_ids.size(); ++i) { + auto qc = _quantize(vertices[i][0], vertices[i][1], vertices[i][2]); + _map[qc] = global_ids[i]; + } + } + + Index getOrInsert(double x, double y, double z) + { + auto qc = _quantize(x, y, z); + auto it = _map.find(qc); + if (it != _map.end()) + return it->second; + + std::array point { x, y, z }; + Index global_id = _model_layer.appendGlobalPoints({ point }); + _mesh_data.vertex_positions_.push_back(point); + _mesh_data.vertex_count_ = static_cast(_mesh_data.vertex_positions_.size()); + _mesh_data.local_to_global_.push_back(global_id); + _map[qc] = global_id; + return global_id; + } + +private: + struct QuantizedCoord { + int64_t ix, iy, iz; + bool operator==(const QuantizedCoord& o) const + { + return ix == o.ix && iy == o.iy && iz == o.iz; + } + }; + + struct CoordHash { + size_t operator()(const QuantizedCoord& c) const + { + size_t h = 0; + h ^= std::hash()(c.ix) + 0x9e3779b9 + (h << 6) + (h >> 2); + h ^= std::hash()(c.iy) + 0x9e3779b9 + (h << 6) + (h >> 2); + h ^= std::hash()(c.iz) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } + }; + + QuantizedCoord _quantize(double x, double y, double z) const + { + return { + static_cast(std::round(x / _tolerance)), + static_cast(std::round(y / _tolerance)), + static_cast(std::round(z / _tolerance)) + }; + } + + MeshData& _mesh_data; + ModelLayer& _model_layer; + double _tolerance; + std::unordered_map _map; +}; + +// 通过全局几何面 ID 从 GeometryRegistry 取得 CAD 面。 +TopoDS_Face getFaceById( + const GeometryRegistry& registry, + GeomFaceId faceId) +{ + const TopoDS_Shape* shape = registry.getFace(faceId); + return shape ? TopoDS::Face(*shape) : TopoDS_Face {}; +} + +// 遍历一个 CAD 面,返回其去重后的全局边 ID。 +std::vector getFaceEdgeIds( + const TopoDS_Face& face, + const GeometryData& geometry) +{ + const auto& edgeMap = + geometry.index.type_maps[GeometrySubshapeIndex::typeIndex(TopAbs_EDGE)]; + + std::vector edgeIds; + for (TopExp_Explorer explorer(face, TopAbs_EDGE); explorer.More(); explorer.Next()) { + int localEdgeId = edgeMap.FindIndex(explorer.Current()); + GeomEdgeId globalEdgeId = geometry.index.edgeGlobalId(localEdgeId); + if (globalEdgeId != kInvalidGeomEdgeId + && std::find(edgeIds.begin(), edgeIds.end(), globalEdgeId) == edgeIds.end()) { + edgeIds.push_back(globalEdgeId); + } + } + return edgeIds; +} + +// 将前端 Combo 索引转换为内部曲面网格类型。 +GmshSurfaceMeshType parseSurfaceMeshType(int meshTypeIndex) +{ + if (meshTypeIndex == static_cast(GmshSurfaceMeshType::QuadDominant)) + return GmshSurfaceMeshType::QuadDominant; + if (meshTypeIndex == static_cast(GmshSurfaceMeshType::StructuredQuadrilateral)) + return GmshSurfaceMeshType::StructuredQuadrilateral; + if (meshTypeIndex != static_cast(GmshSurfaceMeshType::Triangle)) + spdlog::warn("GmshMesh: invalid mesh type {}, using triangle", meshTypeIndex); + return GmshSurfaceMeshType::Triangle; +} + +// 返回日志中使用的曲面网格类型名称。 +const char* surfaceMeshTypeName(GmshSurfaceMeshType meshType) +{ + if (meshType == GmshSurfaceMeshType::QuadDominant) + return "quad-dominant"; + if (meshType == GmshSurfaceMeshType::StructuredQuadrilateral) + return "structured-quad"; + return "triangle"; +} + +// 逐条导入当前面的 OCC 边,并直接使用公开 API 返回的 tag 建立 CAD 边映射。 +// 后续导入整个面时,Gmsh 会通过内部 Shape 映射复用这些已经绑定的曲线 tag。 +std::map importGmshEdges(const TopoDS_Face& face, + const GeometryData& geometry) +{ + std::map result; + const auto& edgeMap = + geometry.index.type_maps[GeometrySubshapeIndex::typeIndex(TopAbs_EDGE)]; + std::vector importedEdgeIds; + + for (TopExp_Explorer explorer(face, TopAbs_EDGE); explorer.More(); explorer.Next()) { + TopoDS_Edge edge = TopoDS::Edge(explorer.Current()); + int localEdgeId = edgeMap.FindIndex(edge); + GeomEdgeId globalEdgeId = geometry.index.edgeGlobalId(localEdgeId); + if (globalEdgeId == kInvalidGeomEdgeId + || std::find(importedEdgeIds.begin(), importedEdgeIds.end(), globalEdgeId) != importedEdgeIds.end()) { + continue; + } + importedEdgeIds.push_back(globalEdgeId); + + gmsh::vectorpair edgeDimTags; + gmsh::model::occ::importShapesNativePointer( + static_cast(&edge), edgeDimTags); + + int gmshTag = 0; + for (const auto& [dim, tag] : edgeDimTags) { + if (dim == 1) { + gmshTag = tag; + break; + } + } + + if (gmshTag <= 0) { + spdlog::warn(" Cannot import OCC edge {}", globalEdgeId); + continue; + } + + result[gmshTag] = globalEdgeId; + spdlog::info(" GMSH edge {} -> OCC edge {}", gmshTag, globalEdgeId); + } + + spdlog::info(" Imported {}/{} OCC edges", + result.size(), importedEdgeIds.size()); + return result; +} + +// 保存结构化划分中一条边的节点数,以及该数量是否由共享边缓存固定。 +struct EdgeTransfiniteInfo { + int gmshTag {}; + int pointCount {}; + bool fixedByExistingMesh {}; +}; + +// 管理一次单面划分使用的 Gmsh 全局会话,确保异常和提前返回时均会释放资源。 +class GmshSession { +public: + GmshSession() + { + if (gmsh::isInitialized()) + throw std::runtime_error("GmshMesh: Gmsh session is already initialized"); + + try { + gmsh::initialize(); + ownsSession_ = true; + } catch (...) { + if (gmsh::isInitialized()) + gmsh::finalize(); + throw; + } + } + + ~GmshSession() + { + if (ownsSession_ && gmsh::isInitialized()) + gmsh::finalize(); + } + + GmshSession(const GmshSession&) = delete; + GmshSession& operator=(const GmshSession&) = delete; + +private: + bool ownsSession_ {}; +}; + +// 设置 Gmsh 数值 option; +void setGmshNumberOption(const std::string& name, double value) +{ + try { + gmsh::option::setNumber(name, value); + } catch (const std::exception& e) { + spdlog::warn("GmshMesh: cannot set option {} = {}: {}", name, value, e.what()); + } +} + +// 把用户参数转换为 Gmsh 全局网格选项。只在这里直接写 Gmsh option,便于后续维护默认值。 +void configureMeshingOptions(const IncrementalMeshTools::GmshMeshParameters& parameters, double meshSize) +{ + double minSize = parameters.minMeshSize > 0.0 ? parameters.minMeshSize : meshSize * 0.5; + double maxSize = parameters.maxMeshSize > 0.0 ? parameters.maxMeshSize : meshSize; + + setGmshNumberOption("Mesh.MeshSizeMin", minSize); + setGmshNumberOption("Mesh.MeshSizeMax", maxSize); + setGmshNumberOption("Mesh.MeshOnlyEmpty", 1); + setGmshNumberOption("Mesh.SaveAll", 1); + setGmshNumberOption("Mesh.Algorithm", parameters.meshAlgorithm); + setGmshNumberOption("Mesh.RecombineAll", 0); + setGmshNumberOption("Mesh.Smoothing", parameters.smoothingSteps); + + setGmshNumberOption("Mesh.AlgorithmSwitchOnFailure", parameters.algorithmSwitchOnFailure); + setGmshNumberOption("Mesh.RecombinationAlgorithm", parameters.recombineAlgorithm); + if (parameters.quadMinQuality > 0.0) + setGmshNumberOption("Mesh.RecombineMinimumQuality", parameters.quadMinQuality); +} + +// 根据曲线长度和目标网格尺寸估算结构化曲线节点数;或者指定的划分段数 +int estimateEdgePointCount(int gmshTag, double meshSize, int structuredEdgeDivisions) +{ + if (structuredEdgeDivisions > 0) + return std::max(2, structuredEdgeDivisions + 1); + + double length = 0.0; + gmsh::model::occ::getMass(1, gmshTag, length); + + int pointCount = static_cast(std::ceil(length / meshSize)) + 2; + if (pointCount < 2) + pointCount = 2; + if (pointCount % 2 == 1) + ++pointCount; + return pointCount; +} + +// 优先采用已有共享边节点数,否则根据曲线长度和网格尺寸估算。 +EdgeTransfiniteInfo makeEdgeTransfiniteInfo( + int gmshTag, + const std::map& gmshToOcc, + const GmshIncrementalMeshState& state, + double meshSize, + int structuredEdgeDivisions) +{ + EdgeTransfiniteInfo info; + info.gmshTag = gmshTag; + + auto occIt = gmshToOcc.find(gmshTag); + if (occIt != gmshToOcc.end()) { + auto cacheIt = state.meshedEdgesCache.find(occIt->second); + if (cacheIt != state.meshedEdgesCache.end() && !cacheIt->second.coords.empty()) { + info.pointCount = static_cast(cacheIt->second.coords.size() / 3); + info.fixedByExistingMesh = true; + return info; + } + } + + info.pointCount = estimateEdgePointCount(gmshTag, meshSize, structuredEdgeDivisions); + return info; +} + +// 协调一对相对边的节点数;两条共享边节点数不一致时拒绝结构化划分。 +bool resolveOppositeEdgePointCount( + const EdgeTransfiniteInfo& first, + const EdgeTransfiniteInfo& opposite, + int& pointCount) +{ + if (first.fixedByExistingMesh && opposite.fixedByExistingMesh) { + if (first.pointCount != opposite.pointCount) { + spdlog::warn("GmshMesh: structured quad rejected, opposite edges {} and {} have {} / {} points", + first.gmshTag, opposite.gmshTag, + first.pointCount, opposite.pointCount); + return false; + } + pointCount = first.pointCount; + return true; + } + + if (first.fixedByExistingMesh) { + pointCount = first.pointCount; + return true; + } + if (opposite.fixedByExistingMesh) { + pointCount = opposite.pointCount; + return true; + } + + pointCount = static_cast((first.pointCount + opposite.pointCount) * 0.5 + 0.5); + if (pointCount < 2) + pointCount = 2; + if (pointCount % 2 == 1) + ++pointCount; + return true; +} + +// 使用 Gmsh 公开 API 配置四边形主导或结构化四边形约束。 +bool configureSurfaceMeshType( + int faceTag, + GmshSurfaceMeshType meshType, + const std::map& gmshToOcc, + const GmshIncrementalMeshState& state, + double meshSize, + const IncrementalMeshTools::GmshMeshParameters& parameters) +{ + if (meshType == GmshSurfaceMeshType::Triangle) + return true; + + if (meshType == GmshSurfaceMeshType::QuadDominant) { + gmsh::model::mesh::setRecombine(2, faceTag, 45.0); + return true; + } + + gmsh::vectorpair boundary; + gmsh::model::getBoundary({ { 2, faceTag } }, boundary, false, false, false); + + std::vector edgeTags; + for (const auto& [dim, tag] : boundary) { + if (dim == 1) + edgeTags.push_back(std::abs(tag)); + } + if (edgeTags.size() != 4) { + spdlog::warn("GmshMesh: structured quad requires 4 boundary edges, surface {} has {}", + faceTag, edgeTags.size()); + return false; + } + + std::array edges {}; + for (std::size_t i = 0; i < edges.size(); ++i) + edges[i] = makeEdgeTransfiniteInfo( + edgeTags[i], gmshToOcc, state, meshSize, parameters.structuredEdgeDivisions); + + int firstPairPointCount = 0; + int secondPairPointCount = 0; + if (!resolveOppositeEdgePointCount(edges[0], edges[2], firstPairPointCount)) + return false; + if (!resolveOppositeEdgePointCount(edges[1], edges[3], secondPairPointCount)) + return false; + + gmsh::model::mesh::setTransfiniteCurve(edges[0].gmshTag, firstPairPointCount); + gmsh::model::mesh::setTransfiniteCurve(edges[2].gmshTag, firstPairPointCount); + gmsh::model::mesh::setTransfiniteCurve(edges[1].gmshTag, secondPairPointCount); + gmsh::model::mesh::setTransfiniteCurve(edges[3].gmshTag, secondPairPointCount); + gmsh::model::mesh::setTransfiniteSurface(faceTag); + gmsh::model::mesh::setRecombine(2, faceTag, 45.0); + return true; +} + +// ---- 注入约束边 ---- +bool injectConstrainedEdge(int gmshTag, + const MeshedEdgeData& nodes, + std::size_t& nodeCounter, + std::size_t& elemCounter, + std::map& vtxNodeMap) +{ + if (nodes.coords.empty()) + return false; + std::size_t nc = nodes.coords.size() / 3; + + std::vector> vtxBnd; + gmsh::model::getBoundary({ { 1, gmshTag } }, vtxBnd, false, false, false); + if (vtxBnd.size() < 2) + return false; + + int vtx0 = std::abs(vtxBnd[0].second); + int vtx1 = std::abs(vtxBnd[1].second); + + double gx0, gy0, gz0; + { + double a, b, c, d, e, f; + gmsh::model::getBoundingBox(0, vtx0, a, b, c, d, e, f); + gx0 = a; + gy0 = b; + gz0 = c; + } + + double distFwd = std::sqrt( + std::pow(gx0 - nodes.coords[0], 2) + std::pow(gy0 - nodes.coords[1], 2) + std::pow(gz0 - nodes.coords[2], 2)); + bool reversed = (distFwd > 1e-6); + + auto orderedCoords = nodes.coords; + auto orderedParams = nodes.paramCoords; + + if (reversed) { + std::vector rev(orderedCoords.size()); + for (std::size_t i = 0; i < nc; ++i) { + std::size_t ri = nc - 1 - i; + rev[i * 3] = orderedCoords[ri * 3]; + rev[i * 3 + 1] = orderedCoords[ri * 3 + 1]; + rev[i * 3 + 2] = orderedCoords[ri * 3 + 2]; + } + orderedCoords = rev; + orderedParams.clear(); + for (std::size_t i = 1; i + 1 < nc; ++i) { + try { + std::vector cc, cp; + gmsh::model::getClosestPoint(1, gmshTag, + { orderedCoords[i * 3], orderedCoords[i * 3 + 1], orderedCoords[i * 3 + 2] }, cc, cp); + if (!cp.empty()) + orderedParams.push_back(cp[0]); + } catch (...) { + orderedParams.push_back(double(i) / double(nc - 1)); + } + } + } + + // 起点 + std::size_t tagV0; + auto it0 = vtxNodeMap.find(vtx0); + if (it0 != vtxNodeMap.end()) { + tagV0 = it0->second; + } else { + tagV0 = nodeCounter++; + gmsh::model::mesh::addNodes(0, vtx0, { tagV0 }, + { orderedCoords[0], orderedCoords[1], orderedCoords[2] }); + vtxNodeMap[vtx0] = tagV0; + } + + // 终点 + std::size_t tagV1; + std::size_t last = nc - 1; + auto it1 = vtxNodeMap.find(vtx1); + if (it1 != vtxNodeMap.end()) { + tagV1 = it1->second; + } else { + tagV1 = nodeCounter++; + gmsh::model::mesh::addNodes(0, vtx1, { tagV1 }, + { orderedCoords[last * 3], orderedCoords[last * 3 + 1], orderedCoords[last * 3 + 2] }); + vtxNodeMap[vtx1] = tagV1; + } + + // 内部节点 + std::vector innerTags; + std::vector innerCoords, innerParams; + + for (std::size_t i = 1; i + 1 < nc; ++i) { + innerTags.push_back(nodeCounter++); + innerCoords.push_back(orderedCoords[i * 3]); + innerCoords.push_back(orderedCoords[i * 3 + 1]); + innerCoords.push_back(orderedCoords[i * 3 + 2]); + } + + if (orderedParams.size() == innerTags.size()) { + innerParams = orderedParams; + } else { + innerParams.clear(); + for (std::size_t i = 0; i < innerTags.size(); ++i) { + std::size_t ci = i + 1; + try { + std::vector cc, cp; + gmsh::model::getClosestPoint(1, gmshTag, + { orderedCoords[ci * 3], orderedCoords[ci * 3 + 1], orderedCoords[ci * 3 + 2] }, cc, cp); + if (!cp.empty()) + innerParams.push_back(cp[0]); + } catch (...) { + innerParams.push_back(double(i + 1) / double(nc - 1)); + } + } + } + + if (!innerTags.empty()) + gmsh::model::mesh::addNodes(1, gmshTag, innerTags, innerCoords, innerParams); + + // 线单元 +std::vector allN; + allN.push_back(tagV0); + for (auto t : innerTags) + allN.push_back(t); + allN.push_back(tagV1); + + std::vector eT, eN; + for (std::size_t i = 0; i + 1 < allN.size(); ++i) { + eT.push_back(elemCounter++); + eN.push_back(allN[i]); + eN.push_back(allN[i + 1]); + } + gmsh::model::mesh::addElementsByType(gmshTag, 1, eT, eN); + return true; +} + +// ---- 提取边节点 ---- +MeshedEdgeData extractEdgeNodes(int gmshTag) +{ + MeshedEdgeData en; + std::vector> vtxBnd; + gmsh::model::getBoundary({ { 1, gmshTag } }, vtxBnd, false, false, false); + if (vtxBnd.size() < 2) + return en; + + { + int vt = std::abs(vtxBnd[0].second); + std::vector nt; + std::vector co, pa; + gmsh::model::mesh::getNodes(nt, co, pa, 0, vt, false, false); + if (!nt.empty()) + en.coords.insert(en.coords.end(), co.begin(), co.begin() + 3); + } + + { + std::vector it; + std::vector ic, ip; + gmsh::model::mesh::getNodes(it, ic, ip, 1, gmshTag, false, true); + en.coords.insert(en.coords.end(), ic.begin(), ic.end()); + en.paramCoords.insert(en.paramCoords.end(), ip.begin(), ip.end()); + } + + { + int vt = std::abs(vtxBnd[1].second); + std::vector nt; + std::vector co, pa; + gmsh::model::mesh::getNodes(nt, co, pa, 0, vt, false, false); + if (!nt.empty()) + en.coords.insert(en.coords.end(), co.begin(), co.begin() + 3); + } + + return en; +} + +// ---- 提取面网格(局部索引)---- +SingleFaceMeshResult extractFaceMesh(int faceTag) +{ + SingleFaceMeshResult result; + result.face_vertices_offset.push_back(0); + + std::vector nodeTags; + std::vector coords, paramCoords; + gmsh::model::mesh::getNodes(nodeTags, coords, paramCoords, 2, faceTag, true, false); + + std::map tagToLocal; + for (size_t i = 0; i < nodeTags.size(); ++i) { + tagToLocal[nodeTags[i]] = result.vertices.size(); + result.vertices.push_back({ coords[3 * i], coords[3 * i + 1], coords[3 * i + 2] }); + } + + std::vector eTypes; + std::vector> eTags, eNodes; + gmsh::model::mesh::getElements(eTypes, eTags, eNodes, 2, faceTag); + + size_t cnt = 0; + for (size_t t = 0; t < eTypes.size(); ++t) { + if (eTypes[t] != 2 && eTypes[t] != 3) + continue; + int npe = (eTypes[t] == 2) ? 3 : 4; + for (size_t i = 0; i < eTags[t].size(); ++i) { + for (int j = 0; j < npe; ++j) + result.face_vertices.push_back(tagToLocal[eNodes[t][i * npe + j]]); + result.face_vertices_offset.push_back(result.face_vertices.size()); + cnt++; + } + } + result.success = (cnt > 0); + spdlog::info(" Extracted: {} nodes, {} elements", nodeTags.size(), cnt); + return result; +} + +// 将当前划分出的 Gmsh 边节点按全局 CAD 边 ID 缓存,供后续相邻面复用。 +void storeNewEdges(GmshIncrementalMeshState& state, const std::map& g2o) +{ + int nNew = 0; + int nShared = 0; + for (auto& [gt, oid] : g2o) { + if (state.meshedEdgeRefCounts.find(oid) == state.meshedEdgeRefCounts.end()) { + auto ed = extractEdgeNodes(gt); + if (!ed.coords.empty()) { + state.meshedEdgesCache[oid] = std::move(ed); + state.meshedEdgeRefCounts[oid] = 1; // 首次创建 + nNew++; + } + } else { + state.meshedEdgeRefCounts[oid]++; // 被另一个面复用 + nShared++; + } + } + spdlog::info("GmshMesh: {} new edges stored, {} edges reused (total cached: {})", nNew, nShared, state.meshedEdgeRefCounts.size()); +} + +void mergeMeshResult( + MeshData& mesh_data, + ModelLayer& model_layer, + SingleFaceMeshResult& result) +{ + if (!result.success) + return; + + if (mesh_data.face_vertices_offset_.empty()) + mesh_data.face_vertices_offset_.push_back(0); + + TempNodeLookup lookup(mesh_data, model_layer, 1e-7); + + std::vector localToGlobal(result.vertices.size()); + for (size_t i = 0; i < result.vertices.size(); ++i) { + localToGlobal[i] = lookup.getOrInsert( + result.vertices[i][0], + result.vertices[i][1], + result.vertices[i][2]); + } + + result.global_face_vertices.clear(); + result.global_face_vertices.reserve(result.face_vertices.size()); + + for (size_t i = 0; i + 1 < result.face_vertices_offset.size(); ++i) { + size_t start = result.face_vertices_offset[i]; + size_t end = result.face_vertices_offset[i + 1]; + for (size_t j = start; j < end; ++j) { + Index globalPointId = localToGlobal[result.face_vertices[j]]; + result.global_face_vertices.push_back(globalPointId); + mesh_data.face_vertices_.push_back(globalPointId); + } + mesh_data.face_vertices_offset_.push_back( + static_cast(mesh_data.face_vertices_.size())); + } + + spdlog::info("GmshMesh: merged total {} nodes, {} cells", + mesh_data.vertex_positions_.size(), + mesh_data.face_vertices_offset_.size() - 1); +} + +// 从 MeshData 中移除指定 Gmsh 面结果的单元,保留其他算法已写入的单元。 +bool removeCachedFaceCells(MeshData& mesh_data, const SingleFaceMeshResult& result) +{ + if (result.global_face_vertices.size() != result.face_vertices.size() + || result.face_vertices_offset.size() < 2) { + spdlog::error("GmshMesh: cached face topology is incomplete"); + return false; + } + + std::set> targetCells; + for (std::size_t i = 0; i + 1 < result.face_vertices_offset.size(); ++i) { + const std::size_t begin = result.face_vertices_offset[i]; + const std::size_t end = result.face_vertices_offset[i + 1]; + if (begin > end || end > result.global_face_vertices.size()) + return false; + targetCells.emplace(result.global_face_vertices.begin() + begin, + result.global_face_vertices.begin() + end); + } + + std::vector keptVertices; + std::vector keptOffsets { 0 }; + for (std::size_t i = 0; i + 1 < mesh_data.face_vertices_offset_.size(); ++i) { + const Index begin = mesh_data.face_vertices_offset_[i]; + const Index end = mesh_data.face_vertices_offset_[i + 1]; + if (begin < 0 || end < begin + || end > static_cast(mesh_data.face_vertices_.size())) { + spdlog::error("GmshMesh: invalid MeshData face offsets"); + return false; + } + + std::vector cell(mesh_data.face_vertices_.begin() + begin, + mesh_data.face_vertices_.begin() + end); + if (targetCells.erase(cell) == 0) + keptVertices.insert(keptVertices.end(), cell.begin(), cell.end()); + keptOffsets.push_back(static_cast(keptVertices.size())); + } + + if (!targetCells.empty()) { + spdlog::error("GmshMesh: cached face cells are missing from MeshData"); + return false; + } + + mesh_data.face_vertices_ = std::move(keptVertices); + mesh_data.face_vertices_offset_ = std::move(keptOffsets); + return true; +} + +// 释放一个 CAD 面占用的边缓存和面缓存,不修改 MeshData。 +bool releaseFaceCache( + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId) +{ + auto faceIt = state.meshedFacesCache.find(faceId); + if (faceIt == state.meshedFacesCache.end()) + return false; + + TopoDS_Face face = getFaceById(model_layer.geomRegistry(), faceId); + for (GeomEdgeId globalEdgeId : getFaceEdgeIds(face, geometry)) { + auto refIt = state.meshedEdgeRefCounts.find(globalEdgeId); + if (refIt == state.meshedEdgeRefCounts.end()) + continue; + + if (--refIt->second <= 0) { + state.meshedEdgeRefCounts.erase(refIt); + state.meshedEdgesCache.erase(globalEdgeId); + } + } + state.meshedFacesCache.erase(faceIt); + return true; +} + +// 复制本次划分会修改的网格数组,用于重划分的候选结果。 +// 提交候选划分产生的网格数组,不影响 MeshData 的属性、边和体数据。 +} // anonymous namespace + +// 公开接口 +SingleFaceMeshResult IncrementalMeshTools::meshSingleFace( + MeshData& mesh_data, + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId, + double meshSize, + const GmshMeshParameters& parameters) +{ + SingleFaceMeshResult result; + GmshSurfaceMeshType meshType = parseSurfaceMeshType(parameters.meshTypeIndex); + + spdlog::info("=== Meshing face {} (size={:.6f}, type={}) ===", + faceId, meshSize, + surfaceMeshTypeName(meshType)); + + TopoDS_Face face = getFaceById(model_layer.geomRegistry(), faceId); + if (face.IsNull()) { + spdlog::error("Face {} is null or invalid", faceId); + return result; + } + GmshSession session; + setGmshNumberOption("General.Terminal", 1); + gmsh::model::add("face_model"); + // 先逐边导入并记录返回 tag,再导入整个面;面导入会复用相同 OCC 边的 tag。 + auto gmshToOcc = importGmshEdges(face, geometry); + + gmsh::vectorpair outDimTags; + gmsh::model::occ::importShapesNativePointer( + static_cast(&face), outDimTags); + gmsh::model::occ::synchronize(); + + std::vector> faceDimTags; + gmsh::model::getEntities(faceDimTags, 2); + if (faceDimTags.empty()) { + spdlog::error(" No face after import"); + return result; + } + int faceTag = faceDimTags[0].second; + + std::size_t nodeCounter = 1, elemCounter = 1; + int shared = 0, free = 0; + std::map vtxNodeMap; + + for (const auto& [gt, oid] : gmshToOcc) { + auto cacheIt = state.meshedEdgesCache.find(oid); + if (state.meshedEdgeRefCounts.count(oid) > 0 && cacheIt != state.meshedEdgesCache.end()) { + if (!injectConstrainedEdge(gt, cacheIt->second, + nodeCounter, elemCounter, vtxNodeMap)) { + spdlog::error("GmshMesh: failed to inject constrained edge {}", oid); + return result; + } + shared++; + } else { + free++; + } + } + spdlog::info(" {} shared, {} free", shared, free); + + configureMeshingOptions(parameters, meshSize); + + try { + if (!configureSurfaceMeshType(faceTag, meshType, gmshToOcc, state, meshSize, parameters)) { + spdlog::warn(" Cannot configure {} mesh", surfaceMeshTypeName(meshType)); + return result; + } + gmsh::model::mesh::generate(2); + } catch (const std::exception& e) { + spdlog::error(" Mesh failed: {}", e.what()); + return result; + } + + // 检查面单元 + { + std::vector ct; + std::vector> cta, cno; + gmsh::model::mesh::getElements(ct, cta, cno, 2, faceTag); + bool has = false; + for (size_t t = 0; t < ct.size(); ++t) + if ((ct[t] == 2 || ct[t] == 3) && !cta[t].empty()) { + has = true; + break; + } + if (!has) { + spdlog::warn(" No surface elements"); + return result; + } + } + + result = extractFaceMesh(faceTag); + if (result.success) { + storeNewEdges(state, gmshToOcc); + mergeMeshResult(mesh_data, model_layer, result); + state.meshedFacesCache[faceId] = result; + } + return result; +} + +double IncrementalMeshTools::estimateMeshSize(const GeometryData& geometry) +{ + if (!geometry.rootShape || geometry.rootShape->IsNull()) + return 10.0; + GProp_GProps props; + BRepGProp::SurfaceProperties(*geometry.rootShape, props); + double area = props.Mass(); + if (area > 0) { + double s = std::sqrt(area) / 10.0; + if (s < 0.01) + s = 0.01; + if (s > 100.0) + s = 100.0; + return s; + } + return 10.0; +} + +std::size_t IncrementalMeshTools::faceCount(const GeometryData& geometry) +{ + if (!geometry.index.built) + return 0; + return static_cast( + geometry.index.type_maps[GeometrySubshapeIndex::typeIndex(TopAbs_FACE)].Extent()); +} + + +bool IncrementalMeshTools::deleteFaceMesh( + MeshData& mesh_data, + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId) +{ + auto it = state.meshedFacesCache.find(faceId); + if (it == state.meshedFacesCache.end()) { + spdlog::warn("Face {} is not meshed or not cached.", faceId); + return false; + } + + if (!removeCachedFaceCells(mesh_data, it->second)) + return false; + if (!releaseFaceCache(geometry, state, model_layer, faceId)) + return false; + + spdlog::info("Deleted Gmsh mesh for face {}. Remaining cells: {}", + faceId, mesh_data.face_vertices_offset_.size() - 1); + return true; +} + +SingleFaceMeshResult IncrementalMeshTools::remeshSingleFace( + MeshData& mesh_data, + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId, + double meshSize, + const GmshMeshParameters& parameters) +{ + if (state.meshedFacesCache.find(faceId) != state.meshedFacesCache.end()) + deleteFaceMesh(mesh_data, geometry, state, model_layer, faceId); + + return meshSingleFace( + mesh_data, geometry, state, model_layer, faceId, meshSize, parameters); +} diff --git a/plugins/algo/GmshPlugin/IncrementalMeshTools.h b/plugins/algo/GmshPlugin/IncrementalMeshTools.h new file mode 100644 index 0000000000000000000000000000000000000000..e7867deacf0efcd7a3d2ea0336a0295d9470beda --- /dev/null +++ b/plugins/algo/GmshPlugin/IncrementalMeshTools.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include "GmshIncrementalMeshState.h" +#include "GeometryData.h" + +struct MeshData; +class ModelLayer; + +namespace IncrementalMeshTools { + +// Gmsh 单面划分参数。空白 UI 参数在 Handler 层会被转换为这里的默认值。 +struct GmshMeshParameters { + double targetMeshSize {}; + double minMeshSize {}; + double maxMeshSize {}; + int meshAlgorithm { 6 }; + int meshTypeIndex {}; + int algorithmSwitchOnFailure {}; + int smoothingSteps {}; + int recombineAlgorithm { 1 }; + double quadMinQuality {}; + int structuredEdgeDivisions {}; +}; + +SingleFaceMeshResult meshSingleFace( + MeshData& mesh_data, + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId, + double meshSize, + const GmshMeshParameters& parameters); + + +SingleFaceMeshResult remeshSingleFace( + MeshData& mesh_data, + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId, + double meshSize, + const GmshMeshParameters& parameters); + +bool deleteFaceMesh( + MeshData& mesh_data, + GeometryData& geometry, + GmshIncrementalMeshState& state, + ModelLayer& model_layer, + GeomFaceId faceId); + +double estimateMeshSize(const GeometryData& geometry); + +std::size_t faceCount(const GeometryData& geometry); + +} // namespace IncrementalMeshTools diff --git a/plugins/algo/GmshPlugin/test/CMakeLists.txt b/plugins/algo/GmshPlugin/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..50306a5a36cf2fa51311942575cf763421bc5d97 --- /dev/null +++ b/plugins/algo/GmshPlugin/test/CMakeLists.txt @@ -0,0 +1,61 @@ +precess_add_test(GmshPluginTest GmshPluginTest.cpp) +precess_test_link_libraries(GmshPluginTest + Data + Systems + Core + TKPrim + TKBRep + TKTopAlgo + TKernel + TKG3d + GmshPluginlib +) + +add_executable(GmshVtkExample "GmshVtkExample.cpp") +target_link_libraries(GmshVtkExample PRIVATE + gmsh::shared + Data + GmshPluginlib + TKDESTEP + TKXSBase + VTK::InteractionStyle + VTK::CommonColor + VTK::RenderingCore + VTK::RenderingUI + VTK::RenderingOpenGL2 + VTK::FiltersGeometry +) + +add_executable(GmshDemo "GmshDemo.cpp") +target_link_libraries(GmshDemo PRIVATE + gmsh::shared + Data + GmshPluginlib + VTK::InteractionStyle + VTK::CommonColor + VTK::RenderingCore + VTK::RenderingUI + VTK::RenderingOpenGL2 + VTK::FiltersGeometry +) +add_custom_command(TARGET GmshDemo POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMENT "Copying gmsh.dll to GmshDemo output directory" +) + +vtk_module_autoinit(TARGETS GmshVtkExample MODULES + VTK::InteractionStyle + VTK::CommonColor + VTK::RenderingCore + VTK::RenderingUI + VTK::RenderingOpenGL2 + VTK::FiltersGeometry +) +add_custom_command(TARGET GmshVtkExample POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMENT "Copying gmsh.dll to GmshVtkExample output directory" +) diff --git a/plugins/algo/GmshPlugin/test/GmshDemo.cpp b/plugins/algo/GmshPlugin/test/GmshDemo.cpp new file mode 100644 index 0000000000000000000000000000000000000000..39e66fedf13f3d5868d4b7646588f3a8dbbe0c68 --- /dev/null +++ b/plugins/algo/GmshPlugin/test/GmshDemo.cpp @@ -0,0 +1,106 @@ +#include "gmsh.h" +#include +#include +#include +#include +#include + +int main() +{ + if (false) { + // 定义矩形的四个角点 + double lc = 0.1; // 网格特征尺寸 + int p1 = gmsh::model::occ::addPoint(0, 0, 0, lc); + int p2 = gmsh::model::occ::addPoint(1, 0, 0, lc); + int p3 = gmsh::model::occ::addPoint(1, 1, 0, lc); + int p4 = gmsh::model::occ::addPoint(0, 1, 0, lc); + + // 定义四条边 + int l1 = gmsh::model::occ::addLine(p1, p2); + int l2 = gmsh::model::occ::addLine(p2, p3); + int l3 = gmsh::model::occ::addLine(p3, p4); + int l4 = gmsh::model::occ::addLine(p4, p1); + + // 创建曲线环 + int loop = gmsh::model::occ::addCurveLoop({ l1, l2, l3, l4 }); + + // 创建平面曲面 + int surface = gmsh::model::occ::addPlaneSurface({ loop }); + + int p5 = gmsh::model::occ::addPoint(0, 0, 0, lc); + int p6 = gmsh::model::occ::addPoint(1, 0, 0, lc); + int p7 = gmsh::model::occ::addPoint(1, 1, 0, lc); + int p8 = gmsh::model::occ::addPoint(0, 1, 0, lc); + + // 定义四条边 + int l5 = gmsh::model::occ::addLine(p5, p6); + int l6 = gmsh::model::occ::addLine(p6, p7); + int l7 = gmsh::model::occ::addLine(p7, p8); + int l8 = gmsh::model::occ::addLine(p8, p5); + + gmsh::model::occ::addPoint(0, 0, 0, lc); + gmsh::model::occ::addPoint(1, 0, 0, lc); + gmsh::model::occ::addPoint(1, 1, 0, lc); + gmsh::model::occ::addPoint(0, 1, 0, lc); + + int s1 = gmsh::model::occ::addRectangle(0, 0, 0, 10, 10); + } + // 初始化 Gmsh + gmsh::initialize(); + gmsh::option::setNumber("General.Terminal", 1); + gmsh::model::add("test_rectangle"); + + std::vector> outDimTags; + std::vector>> outDimTagsMap; + gmsh::model::occ::importShapes("E:/VSProject/_models/models/step_boundary_colors.stp", outDimTags); + gmsh::model::occ::synchronize(); + + gmsh::option::setNumber("Mesh.MeshOnlyEmpty", 1); + gmsh::option::setNumber("Mesh.MeshOnlyVisible", 1); + gmsh::option::setNumber("Mesh.SaveAll", 1); + + // 禁用从顶点继承尺寸,完全由Field控制 + gmsh::option::setNumber("Mesh.MeshSizeFromPoints", 0); + gmsh::option::setNumber("Mesh.MeshSizeFromCurvature", 0); + gmsh::option::setNumber("Mesh.MeshSizeExtendFromBoundary", 0); + // 开启后:Field设置的尺寸会自动扩展到边上 + gmsh::vectorpair faces; + gmsh::model::getEntities(faces, 2); + gmsh::model::setVisibility(faces, 0,true); + + std::set meshedEdges; + std::set meshedFaces; + + for (auto& [dim, tag] : faces) { + double size = 5.0 + tag * 10.0; // 每个face不同尺寸 + + // 用 Field 控制面内部尺寸 + // MathEval常数场 + int fieldTag = gmsh::model::mesh::field::add("MathEval"); + gmsh::model::mesh::field::setString(fieldTag, "F", + std::to_string(size)); + gmsh::model::mesh::field::setAsBackgroundMesh(fieldTag); + + // 显示并网格化当前face + gmsh::model::setVisibility({ { 2, tag } }, 1,true); + gmsh::model::mesh::generate(2); + meshedFaces.insert(tag); + + // 清理Field,避免影响下一个face + gmsh::model::mesh::field::remove(fieldTag); + + // 记录已网格化的边 + gmsh::vectorpair edges; + gmsh::model::getBoundary({ { 2, tag } }, edges, false, false, false); + for (auto& [d, t] : edges) { + if (d == 1) + meshedEdges.insert(std::abs(t)); + } + + gmsh::write("final_mesh.obj"); + int i = 0; + } + + gmsh::finalize(); + return 0; +} \ No newline at end of file diff --git a/plugins/algo/GmshPlugin/test/GmshPluginTest.cpp b/plugins/algo/GmshPlugin/test/GmshPluginTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1e7009c5830b86aa19c6b7a470a7e7f058dae6ca --- /dev/null +++ b/plugins/algo/GmshPlugin/test/GmshPluginTest.cpp @@ -0,0 +1,148 @@ +#include + +#include "GmshMeshHandler.h" +#include "IncrementalMeshTools.h" + +#include "ArgObject.h" +#include "ComponentData.h" +#include "GeometryData.h" +#include "ModelIOSystemBase.h" +#include "ModelLayer.h" +#include "Selection.h" + +#include +#include + +#include + +namespace { + +// 测试用 IO 系统,只记录插件读写请求,避免真实导入导出影响用例状态。 +class MockIOSystem : public systems::io::ModelIOSystemBase { +public: + void read(const std::filesystem::path& path, const std::string& file_type, const std::vector& args) override + { + spdlog::info("[MockIO] read file: {}", path.string()); + } + + void write(Index model, const std::filesystem::path& path, const std::string& file_type, const std::vector& args) override + { + spdlog::info("[MockIO] write model to: {}", path.string()); + } + + void writeComponents(const std::vector& component_ids, + const std::filesystem::path& path, + const std::string& file_type, + const std::vector& args) override + { + spdlog::info("[MockIO] write components to: {}", path.string()); + } +}; + +} // namespace + +TEST_CASE("GmshMeshHandler Execution Test", "[GmshPlugin]") +{ + spdlog::set_level(spdlog::level::info); + + BRepPrimAPI_MakeBox boxMaker(10.0, 10.0, 10.0); + boxMaker.Build(); + REQUIRE(boxMaker.IsDone() == true); + + auto geometryData = std::make_unique(); + geometryData->rootShape = std::make_unique(boxMaker.Shape()); + + ComponentDatas components; + auto component = std::make_unique(); + component->name = "box"; + component->geometry = std::move(geometryData); + components.push_back(std::move(component)); + + ModelLayer modelLayer; + Index modelId = modelLayer.addModel("box", std::move(components)); + auto componentIds = modelLayer.modelById(modelId)->componentIds(); + REQUIRE(componentIds.size() == 1); + + auto componentOp = modelLayer.getComponentOperator(componentIds[0]); + REQUIRE(componentOp.has_value()); + + MockIOSystem mockIo; + systems::algo::HandlerContext context { mockIo, *componentOp }; + + ComponentData* comp = modelLayer.findComponent(componentIds[0]); + REQUIRE(comp != nullptr); + comp->geometry->ensureIndexBuilt(modelLayer.geomRegistry()); + auto selection = std::make_shared(); + selection->type = ElementEnum::GeometryFace; + selection->ids = { + comp->geometry->index.faceGlobalId(1), + comp->geometry->index.faceGlobalId(2) + }; + + std::vector args; + args.push_back(core::ArgObject::create(selection)); + args.push_back(core::ArgObject::create("2.0")); + + systems::algo::GmshMeshHandler handler; + handler.execute(context, args); + + comp = modelLayer.findComponent(componentIds[0]); + REQUIRE(comp != nullptr); + REQUIRE(comp->mesh != nullptr); + REQUIRE(comp->geometry != nullptr); + REQUIRE_FALSE(comp->mesh->local_to_global_.empty()); + REQUIRE_FALSE(comp->mesh->face_vertices_.empty()); + REQUIRE(comp->mesh->face_vertices_offset_.size() > 1); +} + +TEST_CASE("GmshMeshHandler rejects invalid current parameters", "[GmshPlugin]") +{ + BRepPrimAPI_MakeBox boxMaker(10.0, 10.0, 10.0); + boxMaker.Build(); + REQUIRE(boxMaker.IsDone()); + + auto geometryData = std::make_unique(); + geometryData->rootShape = std::make_unique(boxMaker.Shape()); + + ComponentDatas components; + auto component = std::make_unique(); + component->geometry = std::move(geometryData); + components.push_back(std::move(component)); + + ModelLayer modelLayer; + Index modelId = modelLayer.addModel("box", std::move(components)); + const Index componentId = modelLayer.modelById(modelId)->componentIds().front(); + auto componentOp = modelLayer.getComponentOperator(componentId); + REQUIRE(componentOp.has_value()); + + MockIOSystem mockIo; + systems::algo::HandlerContext context { mockIo, *componentOp }; + ComponentData* comp = modelLayer.findComponent(componentId); + REQUIRE(comp != nullptr); + comp->geometry->ensureIndexBuilt(modelLayer.geomRegistry()); + auto selection = std::make_shared(); + selection->type = ElementEnum::GeometryFace; + selection->ids = { comp->geometry->index.faceGlobalId(1) }; + + std::vector args { + core::ArgObject::create(selection), + core::ArgObject::create(0), + core::ArgObject::create("1.0"), + core::ArgObject::create("2.0"), + core::ArgObject::create("1.0"), + core::ArgObject::create(0), + core::ArgObject::create(0), + core::ArgObject::create(""), + core::ArgObject::create(0), + core::ArgObject::create(""), + core::ArgObject::create(""), + core::ArgObject::create(false) + }; + + systems::algo::GmshMeshHandler handler; + handler.execute(context, args); + + comp = modelLayer.findComponent(componentId); + REQUIRE(comp != nullptr); + REQUIRE(comp->mesh == nullptr); +} diff --git a/plugins/algo/GmshPlugin/test/GmshVtkExample.cpp b/plugins/algo/GmshPlugin/test/GmshVtkExample.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c15f2bfc484147589155ea86e85d1b99689534bd --- /dev/null +++ b/plugins/algo/GmshPlugin/test/GmshVtkExample.cpp @@ -0,0 +1,297 @@ +#include + +#include "GeometryData.h" +#include "IncrementalMeshTools.h" +#include "MeshData.h" +#include "ModelLayer.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// VTK 按键回调和示例主循环共享的运行状态。 +struct AppContext { + GeometryData geometry; + GmshIncrementalMeshState gmshState; + MeshData meshData; + ModelLayer modelLayer; + vtkSmartPointer polyData; + vtkRenderWindow* window {}; + std::size_t currentIndex { 0 }; + double meshSize { 10.0 }; +}; + +// 示例加载 STEP, +static bool loadStepGeometry(const std::string& path, GeometryData& geometry, GeometryRegistry& registry) +{ + STEPControl_Reader reader; + if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) + return false; + + reader.TransferRoots(); + geometry.rootShape = std::make_unique(reader.OneShape()); + if (geometry.rootShape->IsNull()) + return false; + + geometry.ensureIndexBuilt(registry); + return true; +} + +// 根据当前 MeshData 的局部到全局点映射,生成“全局点 ID -> 本地点序号”的查询表。 +static std::unordered_map buildGlobalToLocalPointMap(const MeshData& mesh) +{ + std::unordered_map globalToLocal; + const auto& globalIds = mesh.local_to_global_; + for (std::size_t i = 0; i < globalIds.size(); ++i) { + globalToLocal[globalIds[i]] = i; + } + return globalToLocal; +} + +// 从 MeshData 重建 vtkPolyData,避免示例程序依赖 app/render 层。 +static void reloadPolyData(AppContext& ctx) +{ + auto points = vtkSmartPointer::New(); + for (const auto& p : ctx.meshData.vertex_positions_) { + points->InsertNextPoint(p[0], p[1], p[2]); + } + + auto polys = vtkSmartPointer::New(); + auto globalToLocal = buildGlobalToLocalPointMap(ctx.meshData); + + for (std::size_t i = 0; i + 1 < ctx.meshData.face_vertices_offset_.size(); ++i) { + std::size_t begin = ctx.meshData.face_vertices_offset_[i]; + std::size_t end = ctx.meshData.face_vertices_offset_[i + 1]; + vtkIdType count = static_cast(end - begin); + if (count <= 0) + continue; + + polys->InsertNextCell(count); + for (std::size_t j = begin; j < end; ++j) { + auto it = globalToLocal.find(ctx.meshData.face_vertices_[j]); + if (it == globalToLocal.end()) { + spdlog::warn("Missing local point for global id {}", ctx.meshData.face_vertices_[j]); + polys->InsertCellPoint(0); + } else { + polys->InsertCellPoint(static_cast(it->second)); + } + } + } + + ctx.polyData->SetPoints(points); + ctx.polyData->SetPolys(polys); + ctx.polyData->Modified(); + ctx.window->Render(); +} + +// 清空已生成网格和 Gmsh 缓存,但保留已经加载的 CAD 形体。 +static void resetGeneratedMesh(AppContext& ctx) +{ + ctx.meshData.init(); + ctx.gmshState = {}; + if (ctx.geometry.rootShape) + ctx.geometry.ensureIndexBuilt(ctx.modelLayer.geomRegistry()); + ctx.currentIndex = 0; + ctx.meshSize = IncrementalMeshTools::estimateMeshSize(ctx.geometry); + reloadPolyData(ctx); +} + +// 保存示例网格时,把全局点 ID 转回文件内的局部点编号。 +static void saveMesh(const MeshData& mesh, const std::string& filename) +{ + if (mesh.vertex_positions_.empty()) { + spdlog::warn("No mesh data to save."); + return; + } + + try { + gmsh::initialize(); + gmsh::model::add("merged"); + int tag = gmsh::model::addDiscreteEntity(2); + + std::vector nodeTags(mesh.vertex_positions_.size()); + std::vector nodeCoords(mesh.vertex_positions_.size() * 3); + auto globalToLocal = buildGlobalToLocalPointMap(mesh); + + for (std::size_t i = 0; i < mesh.vertex_positions_.size(); ++i) { + nodeTags[i] = i + 1; + nodeCoords[i * 3] = mesh.vertex_positions_[i][0]; + nodeCoords[i * 3 + 1] = mesh.vertex_positions_[i][1]; + nodeCoords[i * 3 + 2] = mesh.vertex_positions_[i][2]; + } + gmsh::model::mesh::addNodes(2, tag, nodeTags, nodeCoords); + + std::vector triTags, triNodes, quadTags, quadNodes; + std::size_t elemTag = 1; + for (std::size_t i = 0; i + 1 < mesh.face_vertices_offset_.size(); ++i) { + std::size_t begin = mesh.face_vertices_offset_[i]; + std::size_t end = mesh.face_vertices_offset_[i + 1]; + std::size_t count = end - begin; + if (count != 3 && count != 4) + continue; + + auto& tags = count == 3 ? triTags : quadTags; + auto& nodes = count == 3 ? triNodes : quadNodes; + tags.push_back(elemTag++); + for (std::size_t j = begin; j < end; ++j) { + auto it = globalToLocal.find(mesh.face_vertices_[j]); + if (it == globalToLocal.end()) { + spdlog::error("Missing local node for global id {}", mesh.face_vertices_[j]); + gmsh::finalize(); + return; + } + nodes.push_back(it->second + 1); + } + } + + if (!triTags.empty()) + gmsh::model::mesh::addElementsByType(tag, 2, triTags, triNodes); + if (!quadTags.empty()) + gmsh::model::mesh::addElementsByType(tag, 3, quadTags, quadNodes); + + gmsh::write(filename); + spdlog::info("Saved: {}", std::filesystem::absolute(filename).string()); + gmsh::finalize(); + } catch (const std::exception& e) { + spdlog::error("Save failed: {}", e.what()); + if (gmsh::isInitialized()) + gmsh::finalize(); + } +} + +static void KeyPressCallback(vtkObject* caller, unsigned long, void* clientData, void*) +{ + auto* ctx = static_cast(clientData); + auto* interactor = static_cast(caller); + std::string key = interactor->GetKeySym(); + + std::size_t total = IncrementalMeshTools::faceCount(ctx->geometry); + + if (key == "space") { + if (ctx->currentIndex >= total) { + spdlog::info("All faces meshed."); + return; + } + + IncrementalMeshTools::GmshMeshParameters parameters; + parameters.targetMeshSize = ctx->meshSize; + const GeomFaceId faceId = + ctx->geometry.index.faceGlobalId(static_cast(ctx->currentIndex) + 1); + + auto result = IncrementalMeshTools::meshSingleFace( + ctx->meshData, + ctx->geometry, + ctx->gmshState, + ctx->modelLayer, + faceId, + ctx->meshSize, + parameters); + + ctx->currentIndex++; + + if (result.success) { + reloadPolyData(*ctx); + if (ctx->meshSize < 50.0) + ctx->meshSize *= 1.5; + spdlog::info("nodes={}, cached_edges={}, next_size={:.4f}", + ctx->meshData.vertex_positions_.size(), + ctx->gmshState.meshedEdgeRefCounts.size(), + ctx->meshSize); + } + } else if (key == "s" || key == "S") { + saveMesh(ctx->meshData, "final_mesh.msh"); + } else if (key == "r" || key == "R") { + resetGeneratedMesh(*ctx); + spdlog::info("Reset mesh, size={:.4f}", ctx->meshSize); + } else if (key == "plus" || key == "equal") { + ctx->meshSize *= 2.0; + spdlog::info("Size: {:.4f}", ctx->meshSize); + } else if (key == "minus") { + ctx->meshSize *= 0.5; + spdlog::info("Size: {:.4f}", ctx->meshSize); + } else if (key == "d" || key == "D") { + if (ctx->currentIndex == 0) { + spdlog::warn("No meshed face to delete."); + return; + } + ctx->currentIndex--; + const GeomFaceId faceId = + ctx->geometry.index.faceGlobalId(static_cast(ctx->currentIndex) + 1); + if (IncrementalMeshTools::deleteFaceMesh( + ctx->meshData, ctx->geometry, ctx->gmshState, + ctx->modelLayer, faceId)) { + reloadPolyData(*ctx); + } + } else if (key == "h" || key == "H") { + spdlog::info("SPACE=mesh, S=save, R=reset, +/-=size, D=delete, H=help"); + } +} + +int main(int argc, char* argv[]) +{ + spdlog::set_level(spdlog::level::info); + + std::string path = argc > 1 ? argv[1] : "E:/VSProject/_models/models/airplane.stp"; + spdlog::info("Loading: {}", path); + + AppContext ctx; + ctx.polyData = vtkSmartPointer::New(); + ctx.meshData.init(); + + if (!loadStepGeometry(path, ctx.geometry, ctx.modelLayer.geomRegistry())) { + spdlog::error("Cannot import: {}", path); + return 1; + } + ctx.meshSize = IncrementalMeshTools::estimateMeshSize(ctx.geometry); + + auto renderer = vtkSmartPointer::New(); + renderer->SetBackground(0.1, 0.15, 0.2); + + auto mapper = vtkSmartPointer::New(); + mapper->SetInputData(ctx.polyData); + + auto actor = vtkSmartPointer::New(); + actor->SetMapper(mapper); + actor->GetProperty()->SetColor(0.85, 0.88, 0.92); + actor->GetProperty()->EdgeVisibilityOn(); + actor->GetProperty()->SetEdgeColor(0.1, 0.2, 0.35); + renderer->AddActor(actor); + + auto window = vtkSmartPointer::New(); + window->AddRenderer(renderer); + window->SetSize(1024, 768); + window->SetWindowName("GmshMesh Test"); + ctx.window = window; + + auto interactor = vtkSmartPointer::New(); + interactor->SetRenderWindow(window); + + auto cb = vtkSmartPointer::New(); + cb->SetCallback(KeyPressCallback); + cb->SetClientData(&ctx); + interactor->AddObserver(vtkCommand::KeyPressEvent, cb); + + window->Render(); + interactor->Start(); + return 0; +}