diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 7f27150001e45d7df45029d8b01104c226541ab1..9f7fdc91f3acefb0de0ba067fcd6a4b550f2320c 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(FeatureDemoPlugin) add_subdirectory(MeshQualityPlugin) +add_subdirectory(MeshRepairPlugin) diff --git a/plugins/feature/MeshRepairPlugin/CMakeLists.txt b/plugins/feature/MeshRepairPlugin/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..00f88691e67492aa3d2d0b5ef1a65159ec2247b5 --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/CMakeLists.txt @@ -0,0 +1,7 @@ +precess_add_feature_plugin(MeshRepairPlugin + SOURCES "MeshRepairHandler.cpp" + PLUGIN_H "MeshRepairPlugin.h" +) +precess_plugin_link_libraries(MeshRepairPlugin CgalSupport) + +add_subdirectory(test) diff --git a/plugins/feature/MeshRepairPlugin/MeshRepairHandler.cpp b/plugins/feature/MeshRepairPlugin/MeshRepairHandler.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6dea34aa4daa994cf7363bc2022505fbcabef4b5 --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/MeshRepairHandler.cpp @@ -0,0 +1,185 @@ +/** + * @file MeshRepairHandler.cpp + * @brief 网格修复处理器:基于 CGAL PMP 的补洞、自交检测与退化清理 + */ + +// CGAL 头文件必须置于所有 OCC 相关头文件之前: +// OCC 的 Standard_Handle.hxx 将 Handle 定义为宏(Handle(X) -> opencascade::handle), +// 若先引入 OCC,宏会污染后续解析的 CGAL/Handle.h 类声明,造成大片级联语法错误 +#include +#include +#include +#include + +#include "MeshRepairHandler.h" +#include "ArgObject.h" +#include "CgalMeshAdapter.h" +#include "ComponentData.h" +#include "ComponentOperator.h" +#include "FeatureContext.h" +#include "FeatureParams.h" +#include "FeatureRegistrar.h" +#include "MeshData.h" +#include "ModelLayer.h" + +#include + +#include + +namespace systems::feature { + +namespace { + +//! @brief 替换当前组件的网格并维护全局点索引与边 id 映射 +//! 流程:释放旧边映射、追加入全局点池、转全局索引、重建边映射、通知 observer +bool replaceComponentMesh(ComponentOperator& comp, std::unique_ptr mesh) +{ + ModelLayer& mgr = comp.manager(); + const Index component_id = comp.componentId(); + ComponentData* component = mgr.findComponent(component_id); + if (!component || !mesh) + return false; + + if (component->mesh) + component->mesh->releaseEdgeIdMap(mgr.edgeIdMap()); + + const Index base = mgr.appendGlobalPoints(mesh->vertex_positions_); + mesh->vertex_count_ = static_cast(mesh->vertex_positions_.size()); + mesh->local_to_global_.resize(mesh->vertex_count_); + for (Index i = 0; i < mesh->vertex_count_; ++i) + mesh->local_to_global_[static_cast(i)] = base + i; + mesh->makePointIdsGlobal(); + std::vector> {}.swap(mesh->vertex_positions_); + mesh->ensureEdgeIdMapBuilt(mgr.edgeIdMap(), component_id); + + component->mesh = std::move(mesh); + comp.notifyChanged(); + return true; +} + +//! @brief 把 CGAL 网格写回当前组件(局部索引 MeshData → 入全局点池替换) +bool writeBack(ComponentOperator& comp, const CgalMesh& sm) +{ + auto out_mesh = std::make_unique(); + fromSurfaceMesh(sm, *out_mesh); + return replaceComponentMesh(comp, std::move(out_mesh)); +} + +std::string fillHoles(ComponentOperator& comp, CgalMesh& sm) +{ + namespace PMP = CGAL::Polygon_mesh_processing; + + // 先收集全部边界环(孔洞),逐个三角化修补;填补只增不删,已收集的边界半边保持有效 + std::vector border_cycles; + PMP::extract_boundary_cycles(sm, std::back_inserter(border_cycles)); + if (border_cycles.empty()) + return std::string("网格无孔洞(未发现边界环)"); + + std::vector new_faces; + std::size_t success_count = 0; + for (const auto h : border_cycles) { + std::vector hole_faces; + PMP::triangulate_hole(sm, h, CGAL::parameters::face_output_iterator(std::back_inserter(hole_faces))); + if (!hole_faces.empty()) { + ++success_count; + new_faces.insert(new_faces.end(), hole_faces.begin(), hole_faces.end()); + } + } + + if (success_count == 0) + return std::string("孔洞修补失败(边界可能为非流形,无法三角化)"); + + if (!writeBack(comp, sm)) + return std::string("错误:修复结果写回组件失败"); + + std::ostringstream oss; + oss << "已修补 " << success_count << "/" << border_cycles.size() + << " 个孔洞,新增 " << new_faces.size() << " 个三角面,网格已更新"; + return oss.str(); +} + +std::string detectSelfIntersections(const CgalMesh& sm) +{ + namespace PMP = CGAL::Polygon_mesh_processing; + + std::vector> intersecting; + PMP::self_intersections(sm, std::back_inserter(intersecting)); + if (intersecting.empty()) + return std::string("未发现自相交面"); + + std::ostringstream oss; + oss << "检测到 " << intersecting.size() << " 对自相交面:\n"; + const std::size_t limit = std::min(intersecting.size(), 10); + for (std::size_t i = 0; i < limit; ++i) + oss << "面 " << intersecting[i].first << " × 面 " << intersecting[i].second << "\n"; + if (intersecting.size() > limit) + oss << "…… 共 " << intersecting.size() << " 对"; + return oss.str(); +} + +std::string removeDegenerateFaces(ComponentOperator& comp, CgalMesh& sm) +{ + namespace PMP = CGAL::Polygon_mesh_processing; + + const std::size_t before = sm.number_of_faces(); + PMP::remove_degenerate_faces(sm); + const std::size_t removed = before - sm.number_of_faces(); + if (removed == 0) + return std::string("未发现退化面"); + + if (!writeBack(comp, sm)) + return std::string("错误:修复结果写回组件失败"); + + std::ostringstream oss; + oss << "已移除 " << removed << " 个退化面,网格已更新"; + return oss.str(); +} + +} // namespace + +void MeshRepairHandler::setup(FeatureRegistrar& reg) +{ + reg.addParameter({ + ArgTypeEnum::Combo, + "操作类型", + "补洞,自交检测,退化清理|0", + "选择要执行的网格修复操作", + }); + reg.addMenuItem({ "功能/网格", "网格修复" }); +} + +std::any MeshRepairHandler::execute(FeatureContext& ctx) +{ + const auto component_id = ctx.activeComponent ? ctx.activeComponent() : std::nullopt; + if (!component_id) + return std::string("未选择 Component"); + auto comp_op = ctx.componentOperator ? ctx.componentOperator(*component_id) : std::nullopt; + if (!comp_op || !comp_op->mesh()) + return std::string("当前 Component 没有网格数据(网格修复仅支持网格模型)"); + + int op_index = 0; + if (const auto* v = ctx.params.value(0).get()) + op_index = *v; + + MeshData* mesh = comp_op->mesh(); + ModelLayer& manager = comp_op->manager(); + + try { + CgalMesh sm = toSurfaceMesh(*mesh, manager.globalPoints()); + switch (op_index) { + case 0: + return fillHoles(*comp_op, sm); + case 1: + return detectSelfIntersections(sm); + case 2: + return removeDegenerateFaces(*comp_op, sm); + default: + return std::string("错误:未知操作类型"); + } + } catch (const std::exception& e) { + spdlog::error("[MeshRepair] CGAL 操作异常: op_index={}, error={}", op_index, e.what()); + return std::string("错误:") + e.what(); + } +} + +} // namespace systems::feature diff --git a/plugins/feature/MeshRepairPlugin/MeshRepairHandler.h b/plugins/feature/MeshRepairPlugin/MeshRepairHandler.h new file mode 100644 index 0000000000000000000000000000000000000000..269884b4f8535f79f4ae84d31283ac4dd79ccfc7 --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/MeshRepairHandler.h @@ -0,0 +1,33 @@ +/** + * @file MeshRepairHandler.h + * @brief 网格修复功能处理器:基于 CGAL PMP 的补洞、自交检测与退化清理 + */ +#ifndef MESH_REPAIR_HANDLER_H +#define MESH_REPAIR_HANDLER_H + +#include "FeatureHandler.h" + +namespace systems::feature { + +/** + * @brief 基于 CGAL Polygon Mesh Processing 的网格修复功能 + * + * 支持三种操作:孔洞填补(三角化边界环)、自相交面检测、退化面清理。 + * 操作经 Combo 参数选择,结果以字符串形式返回。 + */ +class MeshRepairHandler : public FeatureHandler { +public: + /** + * @brief 注册操作类型参数与功能菜单 + */ + void setup(FeatureRegistrar& reg) override; + + /** + * @brief 执行所选修复操作并返回结果文本 + */ + std::any execute(FeatureContext& ctx) override; +}; + +} // namespace systems::feature + +#endif // MESH_REPAIR_HANDLER_H diff --git a/plugins/feature/MeshRepairPlugin/MeshRepairPlugin.h b/plugins/feature/MeshRepairPlugin/MeshRepairPlugin.h new file mode 100644 index 0000000000000000000000000000000000000000..fb6c60300c4d69438418a43d28ed7f5a9e7bf85e --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/MeshRepairPlugin.h @@ -0,0 +1,26 @@ +#pragma once + +#include "HandlerCreatorDestroyerFactory.h" +#include "MeshRepairHandler.h" +#include "PluginBase.h" + +#include + +namespace systems::feature { + +/** + * @brief 网格修复 Feature 插件入口 + */ +class MeshRepairPlugin : public QObject, public PluginBase { + Q_OBJECT + Q_INTERFACES(systems::PluginBase) + Q_PLUGIN_METADATA(IID "com.PreCess.systems.feature.MeshRepairPlugin/1.0" FILE "MeshRepairPlugin.json") + +private: + const HandlerCreatorDestroyer& getHandlerCreatorDestroyer() noexcept override final + { + return HandlerCreatorDestroyerFactory::get(); + } +}; + +} diff --git a/plugins/feature/MeshRepairPlugin/MeshRepairPlugin.json b/plugins/feature/MeshRepairPlugin/MeshRepairPlugin.json new file mode 100644 index 0000000000000000000000000000000000000000..c7b874e906dc8502e16dcaaee83f7065e4765770 --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/MeshRepairPlugin.json @@ -0,0 +1,8 @@ +{ + "system": "FeatureSystem", + "handler": { + "name": "MeshRepair", + "display_name": "网格修复", + "description": "基于 CGAL PMP 的补洞、自交检测与退化清理" + } +} diff --git a/plugins/feature/MeshRepairPlugin/test/CMakeLists.txt b/plugins/feature/MeshRepairPlugin/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..89f375a7a0a95b1efbbe15b1d55c64396847ee42 --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/test/CMakeLists.txt @@ -0,0 +1,8 @@ +precess_add_test(TestMeshRepairPlugin TestMeshRepairPlugin.cpp) +precess_test_link_libraries( + TestMeshRepairPlugin + MeshRepairPluginlib + CgalSupport + FeatureSystem + Data +) diff --git a/plugins/feature/MeshRepairPlugin/test/TestMeshRepairPlugin.cpp b/plugins/feature/MeshRepairPlugin/test/TestMeshRepairPlugin.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6718d252881c0fc21909ca78cfedfc82efa782af --- /dev/null +++ b/plugins/feature/MeshRepairPlugin/test/TestMeshRepairPlugin.cpp @@ -0,0 +1,170 @@ +/** + * @file TestMeshRepairPlugin.cpp + * @brief MeshRepairHandler 的单元测试:补洞、自交检测、退化清理 + */ + +#include "MeshRepairHandler.h" +#include "ArgObject.h" +#include "ComponentData.h" +#include "EventBus.h" +#include "FeatureSystem.h" +#include "MeshData.h" +#include "ModelLayer.h" + +#include + +#include +#include +#include + +using namespace systems; +using namespace systems::feature; + +namespace { + +//! @brief 缺底面的四棱锥(底面四边形边界 = 1 个孔洞) +MeshData makeOpenPyramidMesh() +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 0.5, 0.5, 1.0 }, + { 0.0, 0.0, 0.0 }, + { 1.0, 0.0, 0.0 }, + { 1.0, 1.0, 0.0 }, + { 0.0, 1.0, 0.0 }, + }; + mesh.face_vertices_ = { 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1 }; + mesh.face_vertices_offset_ = { 0, 3, 6, 9, 12 }; + return mesh; +} + +//! @brief 闭合四面体(无孔洞对照) +MeshData makeClosedTetMesh() +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 0.0, 0.0, 0.0 }, + { 1.0, 0.0, 0.0 }, + { 0.0, 1.0, 0.0 }, + { 0.0, 0.0, 1.0 }, + }; + mesh.face_vertices_ = { 0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3 }; + mesh.face_vertices_offset_ = { 0, 3, 6, 9, 12 }; + return mesh; +} + +//! @brief 两个空间交叉的三角形(B 竖直穿过 A 内部) +MeshData makeCrossingTrianglesMesh() +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 0.0, 0.0, 0.0 }, + { 2.0, 0.0, 0.0 }, + { 1.0, 2.0, 0.0 }, + { 1.0, 0.5, -1.0 }, + { 1.0, 0.5, 1.0 }, + { 1.0, -1.0, 0.0 }, + }; + mesh.face_vertices_ = { 0, 1, 2, 3, 4, 5 }; + mesh.face_vertices_offset_ = { 0, 3, 6 }; + return mesh; +} + +//! @brief 一个正常三角面 + 一个共线零面积退化面 +MeshData makeDegenerateMesh() +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 0.0, 0.0, 0.0 }, + { 1.0, 0.0, 0.0 }, + { 0.0, 1.0, 0.0 }, + { 2.0, 0.0, 0.0 }, + { 3.0, 0.0, 0.0 }, + { 4.0, 0.0, 0.0 }, + }; + mesh.face_vertices_ = { 0, 1, 2, 3, 4, 5 }; + mesh.face_vertices_offset_ = { 0, 3, 6 }; + return mesh; +} + +struct RepairResult { + std::string text; + Index face_count; //> 写回后组件面数(未写回则为原面数) +}; + +HandlerMetaData repairMetaData() +{ + HandlerMetaData metadata; + metadata.name = "MeshRepair"; + metadata.display_name = "网格修复"; + return metadata; +} + +//! @brief 在单组件模型上通过 FeatureSystem 执行修复操作 +RepairResult executeRepair(MeshData mesh_data, int op_index) +{ + auto mesh = std::make_unique(std::move(mesh_data)); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + ModelLayer mgr; + const Index model_id = mgr.addModel("test", std::move(comps)); + const auto cids = mgr.modelById(model_id)->componentIds(); + const Index comp_id = cids[0]; + + core::EventBus bus; + FeatureSystem feature_system(mgr, bus); + + FeatureSystem::SystemHandlerPtr handler { new MeshRepairHandler }; + feature_system.registerHandler(repairMetaData(), std::move(handler)); + feature_system.setActiveComponentProvider( + [comp_id]() { return std::optional { comp_id }; }); + + feature_system.setParameter( + "MeshRepair", 0, core::ArgObject::create(op_index)); + + const auto result_any = feature_system.invoke("MeshRepair"); + const auto* result = std::any_cast(&result_any); + + ComponentData* comp = mgr.findComponent(comp_id); + RepairResult r; + r.text = result ? *result : std::string(); + r.face_count = comp && comp->mesh + ? static_cast(comp->mesh->face_vertices_offset_.size()) - 1 + : -1; + return r; +} + +} // namespace + +TEST_CASE("MeshRepair fills a hole in an open pyramid", "[MeshRepairPlugin]") +{ + const RepairResult r = executeRepair(makeOpenPyramidMesh(), 0); + REQUIRE(r.text.find("已修补 1/1 个孔洞") != std::string::npos); + REQUIRE(r.face_count == 6); // 4 侧面 + 底面四边形补成 2 个三角面 +} + +TEST_CASE("MeshRepair reports no holes for a closed mesh", "[MeshRepairPlugin]") +{ + const RepairResult r = executeRepair(makeClosedTetMesh(), 0); + REQUIRE(r.text.find("无孔洞") != std::string::npos); +} + +TEST_CASE("MeshRepair detects self intersections", "[MeshRepairPlugin]") +{ + const RepairResult r = executeRepair(makeCrossingTrianglesMesh(), 1); + REQUIRE(r.text.find("检测到") != std::string::npos); + REQUIRE(r.text.find("1 对") != std::string::npos); +} + +TEST_CASE("MeshRepair removes degenerate faces", "[MeshRepairPlugin]") +{ + const RepairResult r = executeRepair(makeDegenerateMesh(), 2); + REQUIRE(r.text.find("已移除 1 个退化面") != std::string::npos); + REQUIRE(r.face_count == 1); +}