From f09406ed423b83f9624a2e7db8110b942c8f5f09 Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Fri, 14 Aug 2026 17:54:19 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(TopologyDiagnostic):=E7=BD=91=E6=A0=BC?= =?UTF-8?q?=E6=8B=93=E6=89=91=E8=AF=8A=E6=96=AD=E7=BB=93=E6=9E=9C=E6=B8=B2?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/CentralRenderArea.qml | 117 ++++++++++- app/model/QModelQuery.cpp | 9 +- app/render/CMakeLists.txt | 3 +- app/render/MeshActor.cpp | 10 + app/render/MeshActor.h | 5 + app/render/MeshActorManager.cpp | 25 +++ app/render/MeshActorManager.h | 9 + app/render/QRenderWindow.cpp | 18 ++ app/render/QRenderWindow.h | 10 + app/render/TopologyDiagnosticActor.cpp | 181 ++++++++++++++++++ app/render/TopologyDiagnosticActor.h | 66 +++++++ core/Core.h | 23 +++ model/data/CMakeLists.txt | 1 + model/data/MeshAdjacency.cpp | 14 ++ model/data/MeshAdjacency.h | 16 ++ model/data/MeshTopologyDiagnostics.cpp | 158 +++++++++++++++ model/data/MeshTopologyDiagnostics.h | 23 +++ model/data/test/CMakeLists.txt | 5 +- .../data/test/TestMeshTopologyDiagnostics.cpp | 80 ++++++++ 19 files changed, 767 insertions(+), 6 deletions(-) create mode 100644 app/render/TopologyDiagnosticActor.cpp create mode 100644 app/render/TopologyDiagnosticActor.h create mode 100644 model/data/MeshTopologyDiagnostics.cpp create mode 100644 model/data/MeshTopologyDiagnostics.h create mode 100644 model/data/test/TestMeshTopologyDiagnostics.cpp diff --git a/app/CentralRenderArea.qml b/app/CentralRenderArea.qml index bd31be7..a62ddeb 100644 --- a/app/CentralRenderArea.qml +++ b/app/CentralRenderArea.qml @@ -15,6 +15,10 @@ Page { property bool faceSelectByAngle: false property real faceSelectAngle: 30.0 + // 当前渲染区域的拓扑诊断开关和二面角筛选范围。 + property real dihedralMinimumAngle: 30.0 + property real dihedralMaximumAngle: 180.0 + // 标记由功能自动开启的属性渲染,避免影响用户手动打开的属性渲染面板。 property bool featureAttributeRenderingActive: false property int featureAttributeRenderingComponentId: -1 @@ -201,7 +205,7 @@ Page { Layout.fillHeight: true onClicked: meshMenu.open() - Timer { id: meshSubCloseTimer; interval: subMenuCloseDelay; onTriggered: { meshFaceMenu.close(); meshTransMenu.close(); meshWireMenu.close() } } + Timer { id: meshSubCloseTimer; interval: subMenuCloseDelay; onTriggered: { meshFaceMenu.close(); meshTransMenu.close(); meshWireMenu.close(); meshTopologyMenu.close() } } Menu { id: meshMenu @@ -211,6 +215,7 @@ Page { meshFaceMenu.close() meshTransMenu.close() meshWireMenu.close() + meshTopologyMenu.close() } MenuItem { @@ -221,6 +226,7 @@ Page { meshSubCloseTimer.stop() meshTransMenu.close() meshWireMenu.close() + meshTopologyMenu.close() meshFaceMenu.popup(meshBtn, meshMenu.width, meshMenu.y + meshMenu.topPadding) } else { meshSubCloseTimer.restart() @@ -236,6 +242,7 @@ Page { meshSubCloseTimer.stop() meshFaceMenu.close() meshWireMenu.close() + meshTopologyMenu.close() meshTransMenu.popup(meshBtn, meshMenu.width, meshMenu.y + meshMenu.topPadding + meshBtn.menuItemHeight) } else { meshSubCloseTimer.restart() @@ -251,6 +258,7 @@ Page { meshSubCloseTimer.stop() meshFaceMenu.close() meshTransMenu.close() + meshTopologyMenu.close() meshWireMenu.popup(meshBtn, meshMenu.width, meshMenu.y + meshMenu.topPadding + 2 * meshBtn.menuItemHeight) } else { meshSubCloseTimer.restart() @@ -258,6 +266,24 @@ Page { } onTriggered: meshWireMenu.popup(meshBtn, meshMenu.width, meshMenu.y + meshMenu.topPadding + 2 * meshBtn.menuItemHeight) } + MenuItem { + id: meshTopologyCat + text: "拓扑诊断 ▶" + onHoveredChanged: { + if (hovered) { + meshSubCloseTimer.stop() + meshFaceMenu.close() + meshTransMenu.close() + meshWireMenu.close() + meshTopologyMenu.popup(meshBtn, meshMenu.width, + meshMenu.y + meshMenu.topPadding + 3 * meshBtn.menuItemHeight) + } else { + meshSubCloseTimer.restart() + } + } + onTriggered: meshTopologyMenu.popup(meshBtn, meshMenu.width, + meshMenu.y + meshMenu.topPadding + 3 * meshBtn.menuItemHeight) + } MenuItem { text: "隐" checkable: true @@ -324,6 +350,95 @@ Page { onTriggered: myItem.setMeshStyle(6) } } + Menu { + id: meshTopologyMenu + + MenuItem { + text: "边界边" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(0, checked) + } + MenuItem { + text: "边界面" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(1, checked) + } + MenuItem { + text: "非流形边" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(2, checked) + } + MenuItem { + text: "非流形点" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(3, checked) + } + MenuItem { + text: "孤立边" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(4, checked) + } + MenuItem { + text: "孤立点" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(5, checked) + } + MenuSeparator {} + MenuItem { + text: "二面角边" + checkable: true + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + onToggled: myItem.setTopologyDiagnosticVisible(6, checked) + } + MenuItem { + id: minimumDihedralItem + text: "最小二面角" + contentItem: RowLayout { + Label { text: minimumDihedralItem.text; Layout.fillWidth: true } + SpinBox { + from: 0 + to: 180 + editable: true + value: root.dihedralMinimumAngle + onValueModified: { + root.dihedralMinimumAngle = value + if (root.dihedralMaximumAngle < value) + root.dihedralMaximumAngle = value + myItem.setDihedralAngleRange(root.dihedralMinimumAngle, + root.dihedralMaximumAngle) + } + } + } + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + } + MenuItem { + id: maximumDihedralItem + text: "最大二面角" + contentItem: RowLayout { + Label { text: maximumDihedralItem.text; Layout.fillWidth: true } + SpinBox { + from: 0 + to: 180 + editable: true + value: root.dihedralMaximumAngle + onValueModified: { + root.dihedralMaximumAngle = value + if (root.dihedralMinimumAngle > value) + root.dihedralMinimumAngle = value + myItem.setDihedralAngleRange(root.dihedralMinimumAngle, + root.dihedralMaximumAngle) + } + } + } + onHoveredChanged: { if (hovered) meshSubCloseTimer.stop() } + } + } } ToolButton { diff --git a/app/model/QModelQuery.cpp b/app/model/QModelQuery.cpp index 244d61d..9f95b7c 100644 --- a/app/model/QModelQuery.cpp +++ b/app/model/QModelQuery.cpp @@ -1,4 +1,5 @@ #include "QModelQuery.h" +#include "MeshTopologyDiagnostics.h" #include "ModelLayer.h" #include "MeshData.h" #include "GeometryData.h" @@ -80,7 +81,8 @@ std::optional QModelQuery::getMeshData(Index model_id) md->face_vertices_, md->face_vertices_offset_, md->edge_vertices_, md->vertex_positions_, - md->vertex_attributes_,md->edge_attributes_,md->face_attributes_,md->solid_attributes_,{},-1 }; + md->vertex_attributes_,md->edge_attributes_,md->face_attributes_,md->solid_attributes_,{},-1, + std::make_shared(MeshTopologyDiagnostics::analyze(comp->mesh_adjacency, *md)) }; // 添加所有块 auto block_datas = std::make_shared(); @@ -123,7 +125,8 @@ std::optional QModelQuery::getMeshDataByComponent(Index component_i md->face_attributes_, md->solid_attributes_, {}, - component_id + component_id, + std::make_shared(MeshTopologyDiagnostics::analyze(comp->mesh_adjacency, *md)) }; auto block_datas = std::make_shared(); @@ -524,4 +527,4 @@ std::optional QModelQuery::resolveGeometrySolidLocalId(Index compon return std::nullopt; return gid; -} \ No newline at end of file +} diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 262acff..8851809 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(vtkPart STATIC MeshActorManagerSelectOp.cpp GeometryActorManagerSelectOp.cpp InteractionService.cpp + TopologyDiagnosticActor.cpp renderStrategy/AttributeOperator.cpp renderStrategy/AttriRenderStrategyScalar.cpp renderStrategy/AttriRenderStrategyVector.cpp @@ -54,4 +55,4 @@ target_link_libraries(renderQml PRIVATE vtk_module_autoinit(TARGETS renderQml MODULES VTK::GUISupportQtQuick VTK::CommonColor VTK::RenderingAnnotation ) -add_subdirectory(test) \ No newline at end of file +add_subdirectory(test) diff --git a/app/render/MeshActor.cpp b/app/render/MeshActor.cpp index a3c53aa..f376948 100644 --- a/app/render/MeshActor.cpp +++ b/app/render/MeshActor.cpp @@ -1,4 +1,5 @@ #include "MeshActor.h" +#include "TopologyDiagnosticActor.h" #include "renderStrategy/AttributeOperator.h" #include "Core.h" #include @@ -114,6 +115,7 @@ MeshActor::MeshActor(vtkRenderer* renderer) this->face_actor_->SetMapper(face_mapper_); this->edge_actor_->SetMapper(edge_mapper_); this->glyph3D_actor_->SetMapper(glyph3D_mapper_); + topology_diagnostics_ = std::make_unique(renderer_); } MeshActor::~MeshActor() @@ -252,6 +254,7 @@ void MeshActor::loadModelData(const MeshDataVtk& model_data) edge_mapper_->SetScalarVisibility(0); face_mapper_->SetScalarVisibility(0); solid_mapper_->SetScalarVisibility(0); + topology_diagnostics_->loadModelData(model_data); } void MeshActor::setVisibility(bool visibility) @@ -267,6 +270,7 @@ bool MeshActor::isVisible() const void MeshActor::setClipPlane(vtkPlane* plane) { + topology_diagnostics_->setClipPlane(plane); if (plane) { if (!clip_plane_) { solid_clipper_->SetInputData(this->solid_data_); @@ -301,6 +305,7 @@ MeshRenderStyle MeshActor::getRenderStyle() const void MeshActor::applyStyle() { + topology_diagnostics_->setMeshVisible(visibility_ && style_ != MeshRenderStyle::Hidden); if (style_ == MeshRenderStyle::Hidden || !visibility_) { solid_actor_->SetVisibility(false); face_actor_->SetVisibility(false); @@ -396,6 +401,11 @@ void MeshActor::applyStyle() } } +TopologyDiagnosticActor& MeshActor::topologyDiagnostics() +{ + return *topology_diagnostics_; +} + void MeshActor::_createSolidUGird(const MeshDataVtk& model_data, vtkPoints& points, vtkUnstructuredGrid& solid_data) { // cells diff --git a/app/render/MeshActor.h b/app/render/MeshActor.h index 7de0994..aab6eb9 100644 --- a/app/render/MeshActor.h +++ b/app/render/MeshActor.h @@ -22,6 +22,7 @@ class vtkUnstructuredGrid; class vtkRenderer; class MeshActorSelectOp; class AttributeOperator; +class TopologyDiagnosticActor; // 保存面属性渲染临时修改 face_mapper_ 前的相对偏移参数 struct FaceAttributeOffsetState { @@ -72,6 +73,9 @@ public: const std::string& attr_name, std::map args); + /** @brief 获取本组件独立的拓扑诊断渲染对象 */ + TopologyDiagnosticActor& topologyDiagnostics(); + private: void applyStyle(); @@ -79,6 +83,7 @@ private: MeshRenderStyle style_ { MeshRenderStyle::FaceWithEdges }; bool visibility_ { true }; std::unique_ptr model_data_; + std::unique_ptr topology_diagnostics_; vtkPlane* clip_plane_ {}; vtkNew edge_clipper_; diff --git a/app/render/MeshActorManager.cpp b/app/render/MeshActorManager.cpp index 1f35a98..765df80 100644 --- a/app/render/MeshActorManager.cpp +++ b/app/render/MeshActorManager.cpp @@ -4,6 +4,7 @@ #include "renderStrategy/AttriRenderStrategyVector.h" #include "renderStrategy/AttriRenderStrategyUV.h" #include "renderStrategy/AttriRenderStrategyRGB.h" +#include "TopologyDiagnosticActor.h" #include #include #include @@ -72,6 +73,11 @@ void MeshActorManager::loadMesh(Index component_id, const MeshDataVtk& model_dat auto& actor = this->component_actors_[component_id]; actor->loadModelData(model_data); actor->setRenderStyle(current_style_); + for (size_t category = 0; category < topology_diagnostic_visibility_.size(); ++category) { + actor->topologyDiagnostics().setCategoryVisible( + static_cast(category), topology_diagnostic_visibility_[category]); + } + actor->topologyDiagnostics().setDihedralAngleRange(dihedral_minimum_, dihedral_maximum_); op_.registerProps(component_id, actor); } @@ -152,3 +158,22 @@ void MeshActorManager::cancelAttri(Index component_id) } } } + +void MeshActorManager::setTopologyDiagnosticVisible(int category, bool visible) +{ + if (category < 0 || category >= static_cast(topology_diagnostic_visibility_.size())) + return; + topology_diagnostic_visibility_[static_cast(category)] = visible; + for (auto& [id, actor] : component_actors_) { + actor->topologyDiagnostics().setCategoryVisible( + static_cast(category), visible); + } +} + +void MeshActorManager::setDihedralAngleRange(double minimum, double maximum) +{ + dihedral_minimum_ = minimum; + dihedral_maximum_ = maximum; + for (auto& [id, actor] : component_actors_) + actor->topologyDiagnostics().setDihedralAngleRange(minimum, maximum); +} diff --git a/app/render/MeshActorManager.h b/app/render/MeshActorManager.h index ac44352..0f7da92 100644 --- a/app/render/MeshActorManager.h +++ b/app/render/MeshActorManager.h @@ -4,6 +4,7 @@ #include "MeshActor.h" #include "MeshActorManagerSelectOp.h" #include +#include #include class vtkRenderer; @@ -32,6 +33,11 @@ public: std::map args); void cancelAttri(Index component_id); + /** @brief 设置一种拓扑诊断类别的窗口级显示状态 */ + void setTopologyDiagnosticVisible(int category, bool visible); + /** @brief 设置二面角诊断边的筛选范围,单位为度 */ + void setDihedralAngleRange(double minimum, double maximum); + MeshActorManagerSelectOp& op() { return op_; } const MeshActorManagerSelectOp& op() const { return op_; } @@ -40,6 +46,9 @@ private: std::unordered_map> component_actors_; vtkRenderer* renderer_ {}; MeshRenderStyle current_style_ { MeshRenderStyle::FaceWithEdges }; + std::array topology_diagnostic_visibility_ {}; + double dihedral_minimum_ { 30.0 }; + double dihedral_maximum_ { 180.0 }; // 渲染窗口共享的标量颜色表,同一时刻只显示当前标量属性的颜色和值域。 vtkNew scalar_bar_; diff --git a/app/render/QRenderWindow.cpp b/app/render/QRenderWindow.cpp index c504452..624d489 100644 --- a/app/render/QRenderWindow.cpp +++ b/app/render/QRenderWindow.cpp @@ -676,6 +676,24 @@ int QRenderWindow::getMeshStyle() return static_cast(mesh_style_); } +void QRenderWindow::setTopologyDiagnosticVisible(int category, bool visible) +{ + dispatch_async([category, visible](vtkRenderWindow* renderWindow, vtkUserData userData) -> void { + Data* vtk = Data::SafeDownCast(userData); + if (vtk->mesh_actor_manager_) + vtk->mesh_actor_manager_->setTopologyDiagnosticVisible(category, visible); + }); +} + +void QRenderWindow::setDihedralAngleRange(double minimum, double maximum) +{ + dispatch_async([minimum, maximum](vtkRenderWindow* renderWindow, vtkUserData userData) -> void { + Data* vtk = Data::SafeDownCast(userData); + if (vtk->mesh_actor_manager_) + vtk->mesh_actor_manager_->setDihedralAngleRange(minimum, maximum); + }); +} + vtkStandardNewMacro(QRenderWindow::Data); void QRenderWindow::injectRenderRefreshCallback() diff --git a/app/render/QRenderWindow.h b/app/render/QRenderWindow.h index e9aec5d..b133afe 100644 --- a/app/render/QRenderWindow.h +++ b/app/render/QRenderWindow.h @@ -127,6 +127,16 @@ public: Q_INVOKABLE void setMeshStyle(int style); int getMeshStyle(); + /** + * @brief 显示或隐藏一种网格拓扑诊断实体 + * @param category 类别序号,依次为边界边、边界面、非流形边、非流形点、孤立边、孤立点、二面角边 + * @param visible 是否显示 + */ + Q_INVOKABLE void setTopologyDiagnosticVisible(int category, bool visible); + + /** @brief 设置二面角诊断边的角度范围,单位为度 */ + Q_INVOKABLE void setDihedralAngleRange(double minimum, double maximum); + Q_INVOKABLE void onModelChanged(Index model_id); Q_INVOKABLE void onComponentChanged(Index component_id); diff --git a/app/render/TopologyDiagnosticActor.cpp b/app/render/TopologyDiagnosticActor.cpp new file mode 100644 index 0000000..97abb50 --- /dev/null +++ b/app/render/TopologyDiagnosticActor.cpp @@ -0,0 +1,181 @@ +#include "TopologyDiagnosticActor.h" + +#include "CoincidentTopology.h" +#include "MeshTopologyDiagnostics.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +size_t categoryIndex(TopologyDiagnosticCategory category) +{ + return static_cast(category); +} + +//! @brief 由端点索引集合构造线数据,复用组件局部点坐标 +void buildEdgeData(vtkPolyData& output, vtkPoints& points, + const std::vector& edges) +{ + vtkNew lines; + for (const TopologyDiagnosticEdge& edge : edges) { + const vtkIdType ids[2] { edge.endpoints[0], edge.endpoints[1] }; + lines->InsertNextCell(2, ids); + } + output.SetPoints(&points); + output.SetLines(lines); + output.Modified(); +} + +//! @brief 由局部点索引集合构造顶点数据 +void buildPointData(vtkPolyData& output, vtkPoints& points, const std::vector& point_ids) +{ + vtkNew vertices; + for (Index point_id : point_ids) { + const vtkIdType id = point_id; + vertices->InsertNextCell(1, &id); + } + output.SetPoints(&points); + output.SetVerts(vertices); + output.Modified(); +} + +//! @brief 由面索引集合构造半透明边界面数据 +void buildFaceData(vtkPolyData& output, vtkPoints& points, + const MeshDataVtk& model_data, const std::vector& face_ids) +{ + vtkNew polygons; + for (Index face : face_ids) { + if (face < 0 || face + 1 >= static_cast(model_data.vtk_face_cells_offset_.size())) + continue; + const Index begin = model_data.vtk_face_cells_offset_[face]; + const Index end = model_data.vtk_face_cells_offset_[face + 1]; + if (end - begin < 3) + continue; + std::vector ids; + ids.reserve(static_cast(end - begin)); + for (Index i = begin; i < end; ++i) + ids.push_back(model_data.vtk_face_cells_[i]); + polygons->InsertNextCell(static_cast(ids.size()), ids.data()); + } + output.SetPoints(&points); + output.SetPolys(polygons); + output.Modified(); +} +} + +TopologyDiagnosticActor::TopologyDiagnosticActor(vtkRenderer* renderer) + : renderer_(renderer) +{ + if (!renderer_) + throw std::invalid_argument("TopologyDiagnosticActor: renderer cannot be null"); + + const std::array, category_count_> colors { { + { 0.1, 0.45, 1.0 }, { 0.1, 0.45, 1.0 }, { 1.0, 0.1, 0.1 }, + { 1.0, 0.0, 0.8 }, { 1.0, 0.5, 0.0 }, { 1.0, 0.5, 0.0 }, { 1.0, 0.9, 0.0 } + } }; + for (size_t i = 0; i < category_count_; ++i) { + mappers_[i]->SetInputData(data_[i]); + mappers_[i]->SetScalarVisibility(false); + mappers_[i]->SetRelativeCoincidentTopologyLineOffsetParameters(0, highlight::LINE_UNITS + 2); + mappers_[i]->SetRelativeCoincidentTopologyPointOffsetParameter(highlight::POINT_UNITS + 2); + mappers_[i]->SetRelativeCoincidentTopologyPolygonOffsetParameters(0, highlight::POLYGON_UNITS - 1); + actors_[i]->SetMapper(mappers_[i]); + actors_[i]->GetProperty()->SetColor(colors[i][0], colors[i][1], colors[i][2]); + actors_[i]->GetProperty()->SetLineWidth(4.0); + actors_[i]->GetProperty()->SetPointSize(9.0); + actors_[i]->PickableOff(); + actors_[i]->SetVisibility(false); + renderer_->AddActor(actors_[i]); + } + actors_[categoryIndex(TopologyDiagnosticCategory::BoundaryFace)]->GetProperty()->SetOpacity(0.35); +} + +TopologyDiagnosticActor::~TopologyDiagnosticActor() +{ + if (!renderer_) + return; + for (auto& actor : actors_) + renderer_->RemoveActor(actor); +} + +void TopologyDiagnosticActor::loadModelData(const MeshDataVtk& model_data) +{ + model_data_ = std::make_unique(model_data); + vtkNew points; + for (const auto& position : model_data.vertex_positions_) + points->InsertNextPoint(position.data()); + + const auto diagnostics = model_data.topology_diagnostics_; + if (!diagnostics) { + for (auto& data : data_) + data->Initialize(); + applyVisibility(); + return; + } + + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::BoundaryEdge)], *points, diagnostics->boundary_edges); + buildFaceData(*data_[categoryIndex(TopologyDiagnosticCategory::BoundaryFace)], *points, model_data, diagnostics->boundary_faces); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::NonManifoldEdge)], *points, diagnostics->non_manifold_edges); + buildPointData(*data_[categoryIndex(TopologyDiagnosticCategory::NonManifoldVertex)], *points, diagnostics->non_manifold_vertices); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::IsolatedEdge)], *points, diagnostics->isolated_edges); + buildPointData(*data_[categoryIndex(TopologyDiagnosticCategory::IsolatedVertex)], *points, diagnostics->isolated_vertices); + rebuildDihedralEdges(); + applyVisibility(); +} + +void TopologyDiagnosticActor::setCategoryVisible(TopologyDiagnosticCategory category, bool visible) +{ + category_visible_[categoryIndex(category)] = visible; + applyVisibility(); +} + +void TopologyDiagnosticActor::setMeshVisible(bool visible) +{ + mesh_visible_ = visible; + applyVisibility(); +} + +void TopologyDiagnosticActor::setClipPlane(vtkPlane* plane) +{ + for (auto& mapper : mappers_) { + mapper->RemoveAllClippingPlanes(); + if (plane) + mapper->AddClippingPlane(plane); + } +} + +void TopologyDiagnosticActor::setDihedralAngleRange(double minimum, double maximum) +{ + dihedral_minimum_ = std::clamp(minimum, 0.0, 180.0); + dihedral_maximum_ = std::clamp(maximum, dihedral_minimum_, 180.0); + rebuildDihedralEdges(); +} + +void TopologyDiagnosticActor::rebuildDihedralEdges() +{ + if (!model_data_ || !model_data_->topology_diagnostics_) + return; + + std::vector filtered; + for (const auto& edge : model_data_->topology_diagnostics_->manifold_edges) { + if (edge.dihedral_angle_degrees >= dihedral_minimum_ + && edge.dihedral_angle_degrees <= dihedral_maximum_) { + filtered.push_back(edge); + } + } + vtkNew points; + for (const auto& position : model_data_->vertex_positions_) + points->InsertNextPoint(position.data()); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::DihedralEdge)], *points, filtered); +} + +void TopologyDiagnosticActor::applyVisibility() +{ + for (size_t i = 0; i < category_count_; ++i) + actors_[i]->SetVisibility(mesh_visible_ && category_visible_[i]); +} diff --git a/app/render/TopologyDiagnosticActor.h b/app/render/TopologyDiagnosticActor.h new file mode 100644 index 0000000..3a969c7 --- /dev/null +++ b/app/render/TopologyDiagnosticActor.h @@ -0,0 +1,66 @@ +/** + * @file TopologyDiagnosticActor.h + * @brief 网格拓扑诊断结果的独立叠加渲染对象 + */ +#pragma once + +#include "Core.h" + +#include +#include +#include +#include +#include + +class vtkRenderer; +class vtkPlane; + +/** + * @brief 可独立开关的拓扑诊断类别 + */ +enum class TopologyDiagnosticCategory { + BoundaryEdge, + BoundaryFace, + NonManifoldEdge, + NonManifoldVertex, + IsolatedEdge, + IsolatedVertex, + DihedralEdge, + Count +}; + +/** + * @brief 把拓扑诊断集合渲染为独立的边、点和面 Actor + */ +class TopologyDiagnosticActor { +public: + explicit TopologyDiagnosticActor(vtkRenderer* renderer); + ~TopologyDiagnosticActor(); + + /** @brief 根据最新网格和诊断结果重建叠加数据 */ + void loadModelData(const MeshDataVtk& model_data); + /** @brief 设置某一诊断类别是否显示 */ + void setCategoryVisible(TopologyDiagnosticCategory category, bool visible); + /** @brief 设置主网格是否可见,诊断层跟随组件可见性 */ + void setMeshVisible(bool visible); + /** @brief 设置与主网格一致的裁剪平面,传入 nullptr 时取消裁剪 */ + void setClipPlane(vtkPlane* plane); + /** @brief 设置二面角边筛选范围,单位为度 */ + void setDihedralAngleRange(double minimum, double maximum); + +private: + static constexpr size_t category_count_ = static_cast(TopologyDiagnosticCategory::Count); + + void rebuildDihedralEdges(); + void applyVisibility(); + + vtkRenderer* renderer_ {}; + bool mesh_visible_ { true }; + double dihedral_minimum_ { 30.0 }; + double dihedral_maximum_ { 180.0 }; + std::array category_visible_ {}; + std::array, category_count_> data_; + std::array, category_count_> mappers_; + std::array, category_count_> actors_; + std::unique_ptr model_data_; +}; diff --git a/core/Core.h b/core/Core.h index 0c8f17b..120bdc6 100644 --- a/core/Core.h +++ b/core/Core.h @@ -47,6 +47,27 @@ enum class SelectMode { */ using Index = int; +/** + * @brief 一条可直接用于诊断渲染的网格边 + */ +struct TopologyDiagnosticEdge { + std::array endpoints; //> 组件内局部点 id + double dihedral_angle_degrees { -1.0 }; //> 无有效二面角时为 -1 +}; + +/** + * @brief 保存一次网格拓扑诊断得到的特殊实体集合 + */ +struct MeshTopologyDiagnosticResult { + std::vector boundary_edges; + std::vector boundary_faces; + std::vector non_manifold_edges; + std::vector non_manifold_vertices; + std::vector isolated_edges; + std::vector isolated_vertices; + std::vector manifold_edges; //> 恰好邻接两个面的边,保存二面角供范围筛选 +}; + using GeomVertexId = int; using GeomEdgeId = int; using GeomFaceId = int; @@ -97,6 +118,8 @@ struct MeshDataVtk { Index component_id { -1 }; + std::shared_ptr topology_diagnostics_; //> 只读派生诊断结果,供渲染层叠加显示 + Index model_block_id(Index block_id) const { return this->model_blocks_->block_datas[block_id].id; diff --git a/model/data/CMakeLists.txt b/model/data/CMakeLists.txt index 626c4ba..5532ff7 100644 --- a/model/data/CMakeLists.txt +++ b/model/data/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(Data STATIC MeshIDMap.cpp ComponentOperator.cpp MeshAdjacency.cpp + MeshTopologyDiagnostics.cpp ) target_include_directories(Data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} # 头文件路径 diff --git a/model/data/MeshAdjacency.cpp b/model/data/MeshAdjacency.cpp index e598097..b1d6205 100644 --- a/model/data/MeshAdjacency.cpp +++ b/model/data/MeshAdjacency.cpp @@ -109,6 +109,17 @@ Index MeshAdjacency::edgeCount(const MeshData& mesh) return static_cast(rows_.size()); } +std::vector MeshAdjacency::edgeTopologies(const MeshData& mesh) +{ + ensureBuilt(mesh); + std::vector result; + result.reserve(rows_.size()); + for (const EdgeRow& row : rows_) { + result.push_back({ row.endpoints, row.adjacent_faces, row.cell_index >= 0 }); + } + return result; +} + void MeshAdjacency::ensureEdgeGlobalIds(MeshIDMap& map, Index component_id, const MeshData& mesh) { ensureBuilt(mesh); @@ -232,6 +243,9 @@ void MeshAdjacency::ensureBuilt(const MeshData& mesh) continue; const Index row_id = resolve_row(p0, p1); face_edge_stable_ids_[begin + j] = rows_[row_id].stable_id; + auto& adjacent_faces = rows_[row_id].adjacent_faces; + if (adjacent_faces.empty() || adjacent_faces.back() != f) + adjacent_faces.push_back(f); } } } diff --git a/model/data/MeshAdjacency.h b/model/data/MeshAdjacency.h index 9b0d5b5..20697da 100644 --- a/model/data/MeshAdjacency.h +++ b/model/data/MeshAdjacency.h @@ -51,6 +51,15 @@ public: EdgeHandle() = default; }; +/** + * @brief 统一边表中一条边的只读拓扑快照 + */ +struct MeshEdgeTopology { + std::array endpoints; //> 排序后的组件内局部点 id + std::vector adjacent_faces; //> 使用该边的面单元 id + bool is_materialized { false }; //> 是否同时存在于 MeshData::edge_vertices_ +}; + class MeshAdjacency { public: MeshAdjacency() = default; @@ -100,6 +109,12 @@ public: //! @brief 当轮边表行数(物化边 + 面边,重合归并后) Index edgeCount(const MeshData& mesh); + /** + * @brief 获取统一边表的只读拓扑快照 + * @return 每条当前有效边的端点、相邻面和物化状态,不暴露内部行号 + */ + std::vector edgeTopologies(const MeshData& mesh); + // —— 全局 id 同步与生命周期 —— /** @@ -124,6 +139,7 @@ private: std::array endpoints; //> 排序端点对(小在前),唯一键 Index cell_index { -1 }; //> 物化边在 edge_vertices_ 中的 cell 序号,-1 表示纯面边 Index stable_id { -1 }; //> 稳定局部边 id + std::vector adjacent_faces; //> 使用该边的面单元 id }; void ensureBuilt(const MeshData& mesh); diff --git a/model/data/MeshTopologyDiagnostics.cpp b/model/data/MeshTopologyDiagnostics.cpp new file mode 100644 index 0000000..208b08b --- /dev/null +++ b/model/data/MeshTopologyDiagnostics.cpp @@ -0,0 +1,158 @@ +#include "MeshTopologyDiagnostics.h" + +#include "MeshData.h" +#include "MeshAdjacency.h" + +#include +#include +#include +#include + +namespace { +//! @brief 计算多边形前三个非共线顶点定义的单位法向 +std::array faceNormal(const MeshData& mesh, Index face) +{ + const Index begin = mesh.face_vertices_offset_[face]; + const Index end = mesh.face_vertices_offset_[face + 1]; + if (end - begin < 3) + return {}; + + const Index p0_id = mesh.face_vertices_[begin]; + if (p0_id < 0 || p0_id >= static_cast(mesh.vertex_positions_.size())) + return {}; + const auto& p0 = mesh.vertex_positions_[p0_id]; + for (Index i = begin + 1; i + 1 < end; ++i) { + const Index p1_id = mesh.face_vertices_[i]; + const Index p2_id = mesh.face_vertices_[i + 1]; + if (p1_id < 0 || p2_id < 0 + || p1_id >= static_cast(mesh.vertex_positions_.size()) + || p2_id >= static_cast(mesh.vertex_positions_.size())) { + continue; + } + const auto& p1 = mesh.vertex_positions_[p1_id]; + const auto& p2 = mesh.vertex_positions_[p2_id]; + const std::array a { p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2] }; + const std::array b { p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2] }; + std::array normal { + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; + const double length = std::sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); + if (length > 0.0) { + for (double& value : normal) + value /= length; + return normal; + } + } + return {}; +} + +//! @brief 计算两个相邻面单位法向的无符号夹角,范围为 [0, 180] +double dihedralAngle(const std::array& first, const std::array& second) +{ + const double first_length = std::sqrt(first[0] * first[0] + first[1] * first[1] + first[2] * first[2]); + const double second_length = std::sqrt(second[0] * second[0] + second[1] * second[1] + second[2] * second[2]); + if (first_length == 0.0 || second_length == 0.0) + return -1.0; + + const double dot = std::clamp(first[0] * second[0] + first[1] * second[1] + first[2] * second[2], -1.0, 1.0); + constexpr double radians_to_degrees = 180.0 / 3.14159265358979323846; + return std::acos(dot) * radians_to_degrees; +} + +//! @brief 判断一个点的一环面是否由一条连通扇或闭合环组成 +bool isNonManifoldVertex(const std::vector& incident_edges) +{ + std::unordered_map> face_graph; + Index boundary_edge_count = 0; + for (const MeshEdgeTopology& edge : incident_edges) { + if (edge.adjacent_faces.size() > 2) + return true; + if (edge.adjacent_faces.size() == 1) + ++boundary_edge_count; + if (edge.adjacent_faces.size() == 2) { + face_graph[edge.adjacent_faces[0]].push_back(edge.adjacent_faces[1]); + face_graph[edge.adjacent_faces[1]].push_back(edge.adjacent_faces[0]); + } else if (edge.adjacent_faces.size() == 1) { + face_graph.try_emplace(edge.adjacent_faces[0]); + } + } + if (face_graph.empty()) + return false; + + std::unordered_set visited; + std::vector pending { face_graph.begin()->first }; + while (!pending.empty()) { + const Index face = pending.back(); + pending.pop_back(); + if (!visited.insert(face).second) + continue; + for (Index neighbor : face_graph[face]) + pending.push_back(neighbor); + } + + // 流形内部点没有边界边;流形边界点恰有两条边界边。一环断裂或分叉均为非流形。 + return visited.size() != face_graph.size() || (boundary_edge_count != 0 && boundary_edge_count != 2); +} +} + +MeshTopologyDiagnosticResult MeshTopologyDiagnostics::analyze(MeshAdjacency& adjacency, const MeshData& mesh) +{ + MeshTopologyDiagnosticResult result; + const Index point_count = static_cast(mesh.vertex_positions_.size()); + std::vector point_used(static_cast(point_count), false); + + const Index face_count = mesh.face_vertices_offset_.empty() + ? 0 + : static_cast(mesh.face_vertices_offset_.size() - 1); + std::vector> normals(static_cast(face_count)); + std::vector boundary_faces(static_cast(face_count), false); + for (Index face = 0; face < face_count; ++face) { + const Index begin = mesh.face_vertices_offset_[face]; + const Index end = mesh.face_vertices_offset_[face + 1]; + if (begin < 0 || end > static_cast(mesh.face_vertices_.size()) || end - begin < 2) + continue; + normals[static_cast(face)] = faceNormal(mesh, face); + } + + const std::vector edges = adjacency.edgeTopologies(mesh); + std::vector> vertex_edges(static_cast(point_count)); + for (const MeshEdgeTopology& edge : edges) { + if (edge.endpoints[0] < 0 || edge.endpoints[1] < 0 + || edge.endpoints[0] >= point_count || edge.endpoints[1] >= point_count + || edge.endpoints[0] == edge.endpoints[1]) { + continue; + } + point_used[static_cast(edge.endpoints[0])] = true; + point_used[static_cast(edge.endpoints[1])] = true; + TopologyDiagnosticEdge diagnostic_edge { edge.endpoints, -1.0 }; + if (edge.adjacent_faces.empty()) { + result.isolated_edges.push_back(diagnostic_edge); + } else if (edge.adjacent_faces.size() == 1) { + result.boundary_edges.push_back(diagnostic_edge); + boundary_faces[static_cast(edge.adjacent_faces.front())] = true; + } else if (edge.adjacent_faces.size() == 2) { + diagnostic_edge.dihedral_angle_degrees = dihedralAngle( + normals[static_cast(edge.adjacent_faces[0])], normals[static_cast(edge.adjacent_faces[1])]); + result.manifold_edges.push_back(diagnostic_edge); + } else { + result.non_manifold_edges.push_back(diagnostic_edge); + } + vertex_edges[static_cast(edge.endpoints[0])].push_back(edge); + vertex_edges[static_cast(edge.endpoints[1])].push_back(edge); + } + + for (Index face = 0; face < face_count; ++face) { + if (boundary_faces[static_cast(face)]) + result.boundary_faces.push_back(face); + } + for (Index point = 0; point < point_count; ++point) { + if (!point_used[static_cast(point)]) { + result.isolated_vertices.push_back(point); + } else if (isNonManifoldVertex(vertex_edges[static_cast(point)])) { + result.non_manifold_vertices.push_back(point); + } + } + return result; +} diff --git a/model/data/MeshTopologyDiagnostics.h b/model/data/MeshTopologyDiagnostics.h new file mode 100644 index 0000000..c873145 --- /dev/null +++ b/model/data/MeshTopologyDiagnostics.h @@ -0,0 +1,23 @@ +/** + * @file MeshTopologyDiagnostics.h + * @brief 网格特殊拓扑实体与二面角边诊断 + */ +#pragma once + +#include "Core.h" + +struct MeshData; +class MeshAdjacency; + +/** + * @brief 计算网格特殊拓扑实体,结果仅为派生数据,不修改原网格 + */ +class MeshTopologyDiagnostics { +public: + /** + * @brief 对面网格和显式边执行拓扑诊断 + * @param mesh 待读取的网格数据 + * @return 可用于渲染的分类结果 + */ + static MeshTopologyDiagnosticResult analyze(MeshAdjacency& adjacency, const MeshData& mesh); +}; diff --git a/model/data/test/CMakeLists.txt b/model/data/test/CMakeLists.txt index 99d231a..276a824 100644 --- a/model/data/test/CMakeLists.txt +++ b/model/data/test/CMakeLists.txt @@ -26,8 +26,11 @@ precess_test_link_libraries(TestGeometryMeshMap Data) precess_add_test(TestMeshAdjacency TestMeshAdjacency.cpp) precess_test_link_libraries(TestMeshAdjacency Data) +precess_add_test(TestMeshTopologyDiagnostics TestMeshTopologyDiagnostics.cpp) +precess_test_link_libraries(TestMeshTopologyDiagnostics Data) + precess_add_test(TestComponentSnapshot TestComponentSnapshot.cpp) precess_test_link_libraries(TestComponentSnapshot DataTest Data) precess_add_test(TestDirtyNotify TestDirtyNotify.cpp) -precess_test_link_libraries(TestDirtyNotify Data) \ No newline at end of file +precess_test_link_libraries(TestDirtyNotify Data) diff --git a/model/data/test/TestMeshTopologyDiagnostics.cpp b/model/data/test/TestMeshTopologyDiagnostics.cpp new file mode 100644 index 0000000..64b4ed5 --- /dev/null +++ b/model/data/test/TestMeshTopologyDiagnostics.cpp @@ -0,0 +1,80 @@ +#include "MeshTopologyDiagnostics.h" + +#include "MeshAdjacency.h" +#include "MeshData.h" + +#include +#include + +TEST_CASE("MeshTopologyDiagnostics classifies boundary and dihedral edges") +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 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 }; + mesh.face_vertices_offset_ = { 0, 3, 6 }; + + MeshAdjacency adjacency; + const MeshTopologyDiagnosticResult result = MeshTopologyDiagnostics::analyze(adjacency, mesh); + + REQUIRE(result.boundary_edges.size() == 4); + REQUIRE(result.boundary_faces.size() == 2); + REQUIRE(result.manifold_edges.size() == 1); + REQUIRE(result.manifold_edges.front().dihedral_angle_degrees == Catch::Approx(0.0)); + REQUIRE(result.non_manifold_edges.empty()); + REQUIRE(result.non_manifold_vertices.empty()); +} + +TEST_CASE("MeshTopologyDiagnostics classifies non manifold edge and vertices") +{ + 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 }, { 0.0, -1.0, 0.0 } + }; + // 三个三角形共同使用边 (0,1)。 + mesh.face_vertices_ = { 0, 1, 2, 1, 0, 3, 0, 1, 4 }; + mesh.face_vertices_offset_ = { 0, 3, 6, 9 }; + + MeshAdjacency adjacency; + const MeshTopologyDiagnosticResult result = MeshTopologyDiagnostics::analyze(adjacency, mesh); + + REQUIRE(result.non_manifold_edges.size() == 1); + REQUIRE(result.non_manifold_vertices.size() == 2); +} + +TEST_CASE("MeshTopologyDiagnostics classifies isolated edge and vertex") +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, { 2.0, 0.0, 0.0 } + }; + mesh.edge_vertices_ = { 0, 1 }; + mesh.face_vertices_offset_ = { 0 }; + + MeshAdjacency adjacency; + const MeshTopologyDiagnosticResult result = MeshTopologyDiagnostics::analyze(adjacency, mesh); + + REQUIRE(result.isolated_edges.size() == 1); + REQUIRE(result.isolated_vertices == std::vector { 2 }); +} + +TEST_CASE("MeshTopologyDiagnostics detects a disconnected vertex fan") +{ + MeshData mesh; + mesh.vertex_positions_ = { + { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0 }, + { -1.0, 0.0, 0.0 }, { 0.0, -1.0, 0.0 } + }; + // 两个三角形只共享点 0,其一环由两个互不连通的面扇组成。 + mesh.face_vertices_ = { 0, 1, 2, 0, 3, 4 }; + mesh.face_vertices_offset_ = { 0, 3, 6 }; + + MeshAdjacency adjacency; + const MeshTopologyDiagnosticResult result = MeshTopologyDiagnostics::analyze(adjacency, mesh); + + REQUIRE(result.non_manifold_edges.empty()); + REQUIRE(result.non_manifold_vertices == std::vector { 0 }); +} -- Gitee From 282ea1dca12ec2e7dfac3bb1788a641da9912a22 Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Fri, 14 Aug 2026 17:55:21 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix(=20MeshActorSelectOp=20):2.=20=E5=9C=A8?= =?UTF-8?q?registerProps=20=E4=B8=AD=E6=B3=A8=E5=86=8C=E6=8B=93=E6=89=91?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=20actor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/render/MeshActorManagerSelectOp.cpp | 24 ++++++++++++++++++++++++ app/render/TopologyDiagnosticActor.cpp | 8 ++++++++ app/render/TopologyDiagnosticActor.h | 3 +++ 3 files changed, 35 insertions(+) diff --git a/app/render/MeshActorManagerSelectOp.cpp b/app/render/MeshActorManagerSelectOp.cpp index 85c17e5..dad7023 100644 --- a/app/render/MeshActorManagerSelectOp.cpp +++ b/app/render/MeshActorManagerSelectOp.cpp @@ -1,6 +1,7 @@ #include "MeshActorManagerSelectOp.h" #include "MeshActor.h" #include "MeshActorManager.h" +#include "TopologyDiagnosticActor.h" #include #include @@ -50,6 +51,16 @@ void MeshActorManagerSelectOp::registerProps(Index component_id, std::shared_ptr addToAllLists(&op.getSolidActor()); addToAllLists(&op.getFaceActor()); addToAllLists(&op.getEdgeActor()); + + // 注册拓扑诊断 actor(非流形边、边界面等) + auto& topo_diag = actor->topologyDiagnostics(); + for (size_t i = 0; i < static_cast(TopologyDiagnosticCategory::Count); ++i) { + auto category = static_cast(i); + if (auto* topo_actor = topo_diag.getActor(category)) { + prop_to_component_[topo_actor] = component_id; + addToAllLists(topo_actor); + } + } } void MeshActorManagerSelectOp::unregisterProps(std::shared_ptr actor) @@ -62,7 +73,20 @@ void MeshActorManagerSelectOp::unregisterProps(std::shared_ptr actor) prop_to_component_.erase(face); prop_to_component_.erase(edge); + // 注销拓扑诊断 actor + std::set topo_actors; + auto& topo_diag = actor->topologyDiagnostics(); + for (size_t i = 0; i < static_cast(TopologyDiagnosticCategory::Count); ++i) { + auto category = static_cast(i); + if (auto* topo_actor = topo_diag.getActor(category)) { + prop_to_component_.erase(topo_actor); + topo_actors.insert(topo_actor); + } + } + removeFromAllLists({ solid, face, edge }); + if (!topo_actors.empty()) + removeFromAllLists(topo_actors); } void MeshActorManagerSelectOp::addToAllLists(vtkProp* prop) diff --git a/app/render/TopologyDiagnosticActor.cpp b/app/render/TopologyDiagnosticActor.cpp index 97abb50..834e8ec 100644 --- a/app/render/TopologyDiagnosticActor.cpp +++ b/app/render/TopologyDiagnosticActor.cpp @@ -156,6 +156,14 @@ void TopologyDiagnosticActor::setDihedralAngleRange(double minimum, double maxim rebuildDihedralEdges(); } +vtkActor* TopologyDiagnosticActor::getActor(TopologyDiagnosticCategory category) const +{ + const size_t index = categoryIndex(category); + if (index >= category_count_) + return nullptr; + return actors_[index]; +} + void TopologyDiagnosticActor::rebuildDihedralEdges() { if (!model_data_ || !model_data_->topology_diagnostics_) diff --git a/app/render/TopologyDiagnosticActor.h b/app/render/TopologyDiagnosticActor.h index 3a969c7..2f0e1f2 100644 --- a/app/render/TopologyDiagnosticActor.h +++ b/app/render/TopologyDiagnosticActor.h @@ -48,6 +48,9 @@ public: /** @brief 设置二面角边筛选范围,单位为度 */ void setDihedralAngleRange(double minimum, double maximum); + /** @brief 获取指定诊断类别的 actor,用于注册到拾取列表 */ + vtkActor* getActor(TopologyDiagnosticCategory category) const; + private: static constexpr size_t category_count_ = static_cast(TopologyDiagnosticCategory::Count); -- Gitee From ad18afee2c82da627e4a2b96a0e7834f07d7989e Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Fri, 14 Aug 2026 18:05:37 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(TopologyDiagnostic):=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8B=93=E6=89=91Actor=E5=9C=A8=E8=A3=81=E5=89=AA=E5=90=8E?= =?UTF-8?q?=E8=A2=AB=E4=B8=BB=E7=BD=91=E6=A0=BC=E9=81=AE=E6=8C=A1=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/render/TopologyDiagnosticActor.cpp | 17 +++++++++++------ app/render/TopologyDiagnosticActor.h | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/render/TopologyDiagnosticActor.cpp b/app/render/TopologyDiagnosticActor.cpp index 834e8ec..f939fd3 100644 --- a/app/render/TopologyDiagnosticActor.cpp +++ b/app/render/TopologyDiagnosticActor.cpp @@ -81,8 +81,8 @@ TopologyDiagnosticActor::TopologyDiagnosticActor(vtkRenderer* renderer) for (size_t i = 0; i < category_count_; ++i) { mappers_[i]->SetInputData(data_[i]); mappers_[i]->SetScalarVisibility(false); - mappers_[i]->SetRelativeCoincidentTopologyLineOffsetParameters(0, highlight::LINE_UNITS + 2); - mappers_[i]->SetRelativeCoincidentTopologyPointOffsetParameter(highlight::POINT_UNITS + 2); + mappers_[i]->SetRelativeCoincidentTopologyLineOffsetParameters(0, highlight::LINE_UNITS); + mappers_[i]->SetRelativeCoincidentTopologyPointOffsetParameter(highlight::POINT_UNITS); mappers_[i]->SetRelativeCoincidentTopologyPolygonOffsetParameters(0, highlight::POLYGON_UNITS - 1); actors_[i]->SetMapper(mappers_[i]); actors_[i]->GetProperty()->SetColor(colors[i][0], colors[i][1], colors[i][2]); @@ -142,10 +142,15 @@ void TopologyDiagnosticActor::setMeshVisible(bool visible) void TopologyDiagnosticActor::setClipPlane(vtkPlane* plane) { - for (auto& mapper : mappers_) { - mapper->RemoveAllClippingPlanes(); - if (plane) - mapper->AddClippingPlane(plane); + for (size_t i = 0; i < category_count_; ++i) { + if (plane) { + // 与主网格面使用同一种提取裁剪管线,保证保留侧的深度和重合拓扑偏移一致。 + clippers_[i]->SetInputData(data_[i]); + clippers_[i]->SetImplicitFunction(plane); + mappers_[i]->SetInputConnection(clippers_[i]->GetOutputPort()); + } else { + mappers_[i]->SetInputData(data_[i]); + } } } diff --git a/app/render/TopologyDiagnosticActor.h b/app/render/TopologyDiagnosticActor.h index 2f0e1f2..769c632 100644 --- a/app/render/TopologyDiagnosticActor.h +++ b/app/render/TopologyDiagnosticActor.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,7 @@ private: double dihedral_maximum_ { 180.0 }; std::array category_visible_ {}; std::array, category_count_> data_; + std::array, category_count_> clippers_; std::array, category_count_> mappers_; std::array, category_count_> actors_; std::unique_ptr model_data_; -- Gitee From 4d6dae611d17ebcd52eba854bf56510b5fa669e9 Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Fri, 14 Aug 2026 19:19:01 +0800 Subject: [PATCH 4/8] =?UTF-8?q?refactor(TopologyDiagnosticActor):=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E6=8E=A5=E5=8F=A3,=E5=86=85=E9=83=A8=E8=AF=8A?= =?UTF-8?q?=E6=96=ADActor=E6=94=B9=E4=B8=BA=E7=A7=81=E6=9C=89=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/render/MeshActorManagerSelectOp.cpp | 39 ++++--------------------- app/render/QRenderWindow.cpp | 23 +++++++++++++-- app/render/TopologyDiagnosticActor.cpp | 8 ----- app/render/TopologyDiagnosticActor.h | 3 -- 4 files changed, 27 insertions(+), 46 deletions(-) diff --git a/app/render/MeshActorManagerSelectOp.cpp b/app/render/MeshActorManagerSelectOp.cpp index dad7023..d51b2d8 100644 --- a/app/render/MeshActorManagerSelectOp.cpp +++ b/app/render/MeshActorManagerSelectOp.cpp @@ -1,7 +1,6 @@ #include "MeshActorManagerSelectOp.h" #include "MeshActor.h" #include "MeshActorManager.h" -#include "TopologyDiagnosticActor.h" #include #include @@ -51,16 +50,6 @@ void MeshActorManagerSelectOp::registerProps(Index component_id, std::shared_ptr addToAllLists(&op.getSolidActor()); addToAllLists(&op.getFaceActor()); addToAllLists(&op.getEdgeActor()); - - // 注册拓扑诊断 actor(非流形边、边界面等) - auto& topo_diag = actor->topologyDiagnostics(); - for (size_t i = 0; i < static_cast(TopologyDiagnosticCategory::Count); ++i) { - auto category = static_cast(i); - if (auto* topo_actor = topo_diag.getActor(category)) { - prop_to_component_[topo_actor] = component_id; - addToAllLists(topo_actor); - } - } } void MeshActorManagerSelectOp::unregisterProps(std::shared_ptr actor) @@ -73,37 +62,21 @@ void MeshActorManagerSelectOp::unregisterProps(std::shared_ptr actor) prop_to_component_.erase(face); prop_to_component_.erase(edge); - // 注销拓扑诊断 actor - std::set topo_actors; - auto& topo_diag = actor->topologyDiagnostics(); - for (size_t i = 0; i < static_cast(TopologyDiagnosticCategory::Count); ++i) { - auto category = static_cast(i); - if (auto* topo_actor = topo_diag.getActor(category)) { - prop_to_component_.erase(topo_actor); - topo_actors.insert(topo_actor); - } - } - removeFromAllLists({ solid, face, edge }); - if (!topo_actors.empty()) - removeFromAllLists(topo_actors); } void MeshActorManagerSelectOp::addToAllLists(vtkProp* prop) { - for (auto& list : pick_lists_) - list->AddItem(prop); + for (auto& list : pick_lists_) { + if (!list->IsItemPresent(prop)) + list->AddItem(prop); + } } void MeshActorManagerSelectOp::removeFromAllLists(const std::set& props) { for (auto& list : pick_lists_) { - list->InitTraversal(); - vtkProp* prop; - while ((prop = list->GetNextProp()) != nullptr) { - if (props.count(prop)) { - list->RemoveItem(prop); - } - } + for (vtkProp* prop : props) + list->RemoveItem(prop); } } diff --git a/app/render/QRenderWindow.cpp b/app/render/QRenderWindow.cpp index 624d489..825fe97 100644 --- a/app/render/QRenderWindow.cpp +++ b/app/render/QRenderWindow.cpp @@ -269,9 +269,28 @@ QQuickVTKItem::vtkUserData QRenderWindow::initializeVTK(vtkRenderWindow* renderW void QRenderWindow::destroyingVTK(vtkRenderWindow* renderWindow, vtkUserData userData) { auto* vtk = Data::SafeDownCast(userData); - if (vtk->renderer_) { - vtk->renderer_->RemoveAllViewProps(); + if (!vtk) + return; + + // 选择与交互服务持有 renderer、manager op 和 pick list,必须在 VTK Data 资源前释放。 + if (vtk->style_) { + vtk->style_->SetInteractionService(nullptr); + vtk->style_->SetSelectManager(nullptr); } + interaction_service_.reset(); + select_manager_.reset(); + + // 裁剪回调保存 MeshActorManager 裸指针,先停用并移除回调,再销毁 manager。 + if (vtk->plane_widget_) { + vtk->plane_widget_->Off(); + vtk->plane_widget_->RemoveObservers(vtkCommand::InteractionEvent); + } + vtk->geometry_actor_manager_.reset(); + vtk->mesh_actor_manager_.reset(); + + if (vtk->renderer_) + vtk->renderer_->RemoveAllViewProps(); + data_ = nullptr; } void QRenderWindow::resetCamera() diff --git a/app/render/TopologyDiagnosticActor.cpp b/app/render/TopologyDiagnosticActor.cpp index f939fd3..bcfe371 100644 --- a/app/render/TopologyDiagnosticActor.cpp +++ b/app/render/TopologyDiagnosticActor.cpp @@ -161,14 +161,6 @@ void TopologyDiagnosticActor::setDihedralAngleRange(double minimum, double maxim rebuildDihedralEdges(); } -vtkActor* TopologyDiagnosticActor::getActor(TopologyDiagnosticCategory category) const -{ - const size_t index = categoryIndex(category); - if (index >= category_count_) - return nullptr; - return actors_[index]; -} - void TopologyDiagnosticActor::rebuildDihedralEdges() { if (!model_data_ || !model_data_->topology_diagnostics_) diff --git a/app/render/TopologyDiagnosticActor.h b/app/render/TopologyDiagnosticActor.h index 769c632..ccfe5df 100644 --- a/app/render/TopologyDiagnosticActor.h +++ b/app/render/TopologyDiagnosticActor.h @@ -49,9 +49,6 @@ public: /** @brief 设置二面角边筛选范围,单位为度 */ void setDihedralAngleRange(double minimum, double maximum); - /** @brief 获取指定诊断类别的 actor,用于注册到拾取列表 */ - vtkActor* getActor(TopologyDiagnosticCategory category) const; - private: static constexpr size_t category_count_ = static_cast(TopologyDiagnosticCategory::Count); -- Gitee From cd2efe04868de7998183ede1c996c98c437add96 Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Sat, 15 Aug 2026 13:49:10 +0800 Subject: [PATCH 5/8] =?UTF-8?q?style():=E4=BF=AE=E6=94=B9=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/model/QModelQuery.cpp | 2 +- model/data/MeshAdjacency.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/model/QModelQuery.cpp b/app/model/QModelQuery.cpp index 9f95b7c..1618eae 100644 --- a/app/model/QModelQuery.cpp +++ b/app/model/QModelQuery.cpp @@ -527,4 +527,4 @@ std::optional QModelQuery::resolveGeometrySolidLocalId(Index compon return std::nullopt; return gid; -} +} \ No newline at end of file diff --git a/model/data/MeshAdjacency.h b/model/data/MeshAdjacency.h index 20697da..76de903 100644 --- a/model/data/MeshAdjacency.h +++ b/model/data/MeshAdjacency.h @@ -57,7 +57,7 @@ public: struct MeshEdgeTopology { std::array endpoints; //> 排序后的组件内局部点 id std::vector adjacent_faces; //> 使用该边的面单元 id - bool is_materialized { false }; //> 是否同时存在于 MeshData::edge_vertices_ + bool is_materialized { false }; //> 是否在 edge_vertices_ 中存在显式边单元,供后续边编辑和修复使用 }; class MeshAdjacency { -- Gitee From e1b01f2386afe926d46550ca6bb03d04c59e55ea Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Sat, 15 Aug 2026 14:43:33 +0800 Subject: [PATCH 6/8] =?UTF-8?q?refactor(ToolofyDiagnostic):=20=E7=B2=BE?= =?UTF-8?q?=E7=AE=80=E6=8B=93=E6=89=91=E8=AF=8A=E6=96=AD=E5=8F=AF=E8=A7=86?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E4=B8=8E=E7=B1=BB=E5=88=AB=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/render/MeshActorManager.h | 3 ++- app/render/TopologyDiagnosticActor.cpp | 30 +++++++++++-------------- app/render/TopologyDiagnosticActor.h | 21 +++++------------ app/render/TopologyDiagnosticCategory.h | 25 +++++++++++++++++++++ model/data/MeshAdjacency.cpp | 2 +- model/data/MeshAdjacency.h | 1 - 6 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 app/render/TopologyDiagnosticCategory.h diff --git a/app/render/MeshActorManager.h b/app/render/MeshActorManager.h index 0f7da92..b31e5e6 100644 --- a/app/render/MeshActorManager.h +++ b/app/render/MeshActorManager.h @@ -3,6 +3,7 @@ #include "Core.h" #include "MeshActor.h" #include "MeshActorManagerSelectOp.h" +#include "TopologyDiagnosticCategory.h" #include #include #include @@ -46,7 +47,7 @@ private: std::unordered_map> component_actors_; vtkRenderer* renderer_ {}; MeshRenderStyle current_style_ { MeshRenderStyle::FaceWithEdges }; - std::array topology_diagnostic_visibility_ {}; + std::array topology_diagnostic_visibility_ {}; double dihedral_minimum_ { 30.0 }; double dihedral_maximum_ { 180.0 }; diff --git a/app/render/TopologyDiagnosticActor.cpp b/app/render/TopologyDiagnosticActor.cpp index bcfe371..005b64f 100644 --- a/app/render/TopologyDiagnosticActor.cpp +++ b/app/render/TopologyDiagnosticActor.cpp @@ -105,25 +105,24 @@ TopologyDiagnosticActor::~TopologyDiagnosticActor() void TopologyDiagnosticActor::loadModelData(const MeshDataVtk& model_data) { - model_data_ = std::make_unique(model_data); - vtkNew points; + diagnostics_ = model_data.topology_diagnostics_; + points_->Reset(); for (const auto& position : model_data.vertex_positions_) - points->InsertNextPoint(position.data()); + points_->InsertNextPoint(position.data()); - const auto diagnostics = model_data.topology_diagnostics_; - if (!diagnostics) { + if (!diagnostics_) { for (auto& data : data_) data->Initialize(); applyVisibility(); return; } - buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::BoundaryEdge)], *points, diagnostics->boundary_edges); - buildFaceData(*data_[categoryIndex(TopologyDiagnosticCategory::BoundaryFace)], *points, model_data, diagnostics->boundary_faces); - buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::NonManifoldEdge)], *points, diagnostics->non_manifold_edges); - buildPointData(*data_[categoryIndex(TopologyDiagnosticCategory::NonManifoldVertex)], *points, diagnostics->non_manifold_vertices); - buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::IsolatedEdge)], *points, diagnostics->isolated_edges); - buildPointData(*data_[categoryIndex(TopologyDiagnosticCategory::IsolatedVertex)], *points, diagnostics->isolated_vertices); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::BoundaryEdge)], *points_, diagnostics_->boundary_edges); + buildFaceData(*data_[categoryIndex(TopologyDiagnosticCategory::BoundaryFace)], *points_, model_data, diagnostics_->boundary_faces); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::NonManifoldEdge)], *points_, diagnostics_->non_manifold_edges); + buildPointData(*data_[categoryIndex(TopologyDiagnosticCategory::NonManifoldVertex)], *points_, diagnostics_->non_manifold_vertices); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::IsolatedEdge)], *points_, diagnostics_->isolated_edges); + buildPointData(*data_[categoryIndex(TopologyDiagnosticCategory::IsolatedVertex)], *points_, diagnostics_->isolated_vertices); rebuildDihedralEdges(); applyVisibility(); } @@ -163,20 +162,17 @@ void TopologyDiagnosticActor::setDihedralAngleRange(double minimum, double maxim void TopologyDiagnosticActor::rebuildDihedralEdges() { - if (!model_data_ || !model_data_->topology_diagnostics_) + if (!diagnostics_) return; std::vector filtered; - for (const auto& edge : model_data_->topology_diagnostics_->manifold_edges) { + for (const auto& edge : diagnostics_->manifold_edges) { if (edge.dihedral_angle_degrees >= dihedral_minimum_ && edge.dihedral_angle_degrees <= dihedral_maximum_) { filtered.push_back(edge); } } - vtkNew points; - for (const auto& position : model_data_->vertex_positions_) - points->InsertNextPoint(position.data()); - buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::DihedralEdge)], *points, filtered); + buildEdgeData(*data_[categoryIndex(TopologyDiagnosticCategory::DihedralEdge)], *points_, filtered); } void TopologyDiagnosticActor::applyVisibility() diff --git a/app/render/TopologyDiagnosticActor.h b/app/render/TopologyDiagnosticActor.h index ccfe5df..2c24289 100644 --- a/app/render/TopologyDiagnosticActor.h +++ b/app/render/TopologyDiagnosticActor.h @@ -5,31 +5,19 @@ #pragma once #include "Core.h" +#include "TopologyDiagnosticCategory.h" #include #include #include #include +#include #include #include class vtkRenderer; class vtkPlane; -/** - * @brief 可独立开关的拓扑诊断类别 - */ -enum class TopologyDiagnosticCategory { - BoundaryEdge, - BoundaryFace, - NonManifoldEdge, - NonManifoldVertex, - IsolatedEdge, - IsolatedVertex, - DihedralEdge, - Count -}; - /** * @brief 把拓扑诊断集合渲染为独立的边、点和面 Actor */ @@ -50,7 +38,7 @@ public: void setDihedralAngleRange(double minimum, double maximum); private: - static constexpr size_t category_count_ = static_cast(TopologyDiagnosticCategory::Count); + static constexpr std::size_t category_count_ = kTopologyDiagnosticCategoryCount; void rebuildDihedralEdges(); void applyVisibility(); @@ -64,5 +52,6 @@ private: std::array, category_count_> clippers_; std::array, category_count_> mappers_; std::array, category_count_> actors_; - std::unique_ptr model_data_; + vtkNew points_; //> 当前组件诊断几何共用的点坐标 + std::shared_ptr diagnostics_; //> 二面角范围变化时重新筛选的只读结果 }; diff --git a/app/render/TopologyDiagnosticCategory.h b/app/render/TopologyDiagnosticCategory.h new file mode 100644 index 0000000..47d3bec --- /dev/null +++ b/app/render/TopologyDiagnosticCategory.h @@ -0,0 +1,25 @@ +/** + * @file TopologyDiagnosticCategory.h + * @brief 定义拓扑诊断可视化类别 + */ +#pragma once + +#include + +/** + * @brief 可独立显示的网格拓扑诊断类别 + */ +enum class TopologyDiagnosticCategory { + BoundaryEdge, + BoundaryFace, + NonManifoldEdge, + NonManifoldVertex, + IsolatedEdge, + IsolatedVertex, + DihedralEdge, + Count +}; + +//! @brief 拓扑诊断类别数量,由枚举末值统一推导 +inline constexpr std::size_t kTopologyDiagnosticCategoryCount + = static_cast(TopologyDiagnosticCategory::Count); diff --git a/model/data/MeshAdjacency.cpp b/model/data/MeshAdjacency.cpp index b1d6205..7b2e484 100644 --- a/model/data/MeshAdjacency.cpp +++ b/model/data/MeshAdjacency.cpp @@ -115,7 +115,7 @@ std::vector MeshAdjacency::edgeTopologies(const MeshData& mesh std::vector result; result.reserve(rows_.size()); for (const EdgeRow& row : rows_) { - result.push_back({ row.endpoints, row.adjacent_faces, row.cell_index >= 0 }); + result.push_back({ row.endpoints, row.adjacent_faces }); } return result; } diff --git a/model/data/MeshAdjacency.h b/model/data/MeshAdjacency.h index 76de903..6df7ddf 100644 --- a/model/data/MeshAdjacency.h +++ b/model/data/MeshAdjacency.h @@ -57,7 +57,6 @@ public: struct MeshEdgeTopology { std::array endpoints; //> 排序后的组件内局部点 id std::vector adjacent_faces; //> 使用该边的面单元 id - bool is_materialized { false }; //> 是否在 edge_vertices_ 中存在显式边单元,供后续边编辑和修复使用 }; class MeshAdjacency { -- Gitee From 41e4c9061c4c65995f9a40783d9d3ce539bb5257 Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Sat, 15 Aug 2026 15:13:08 +0800 Subject: [PATCH 7/8] =?UTF-8?q?refactor(TopologyDignostics):=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E9=A2=9C=E8=89=B2=EF=BC=8C=E9=9D=9E=E6=B5=81=E5=BD=A2?= =?UTF-8?q?=E7=82=B9=E4=BB=85=E8=A1=A8=E7=A4=BA=E7=8B=AC=E7=AB=8B=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E7=8E=AF=E9=9D=A2=E6=89=87=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/render/TopologyDiagnosticActor.cpp | 9 +++++++-- model/data/MeshTopologyDiagnostics.cpp | 3 ++- model/data/test/TestMeshTopologyDiagnostics.cpp | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/render/TopologyDiagnosticActor.cpp b/app/render/TopologyDiagnosticActor.cpp index 005b64f..3c535e4 100644 --- a/app/render/TopologyDiagnosticActor.cpp +++ b/app/render/TopologyDiagnosticActor.cpp @@ -75,8 +75,13 @@ TopologyDiagnosticActor::TopologyDiagnosticActor(vtkRenderer* renderer) throw std::invalid_argument("TopologyDiagnosticActor: renderer cannot be null"); const std::array, category_count_> colors { { - { 0.1, 0.45, 1.0 }, { 0.1, 0.45, 1.0 }, { 1.0, 0.1, 0.1 }, - { 1.0, 0.0, 0.8 }, { 1.0, 0.5, 0.0 }, { 1.0, 0.5, 0.0 }, { 1.0, 0.9, 0.0 } + { 0.1, 0.8, 0.2 }, // 边界边:绿色 + { 0.55, 1.0, 0.65 }, // 边界面:淡绿色 + { 1.0, 0.0, 0.0 }, // 非流形边:红色 + { 1.0, 0.0, 0.8 }, // 非流形点:品红色 + { 1.0, 0.5, 0.0 }, // 孤立边:橙色 + { 1.0, 0.5, 0.0 }, // 孤立点:橙色 + { 1.0, 0.9, 0.0 } // 二面角边:黄色 } }; for (size_t i = 0; i < category_count_; ++i) { mappers_[i]->SetInputData(data_[i]); diff --git a/model/data/MeshTopologyDiagnostics.cpp b/model/data/MeshTopologyDiagnostics.cpp index 208b08b..23cd0f1 100644 --- a/model/data/MeshTopologyDiagnostics.cpp +++ b/model/data/MeshTopologyDiagnostics.cpp @@ -67,8 +67,9 @@ bool isNonManifoldVertex(const std::vector& incident_edges) std::unordered_map> face_graph; Index boundary_edge_count = 0; for (const MeshEdgeTopology& edge : incident_edges) { + // 将非流形边及其端点归入边类别,非流形点仅表示独立的一环面扇异常。 if (edge.adjacent_faces.size() > 2) - return true; + return false; if (edge.adjacent_faces.size() == 1) ++boundary_edge_count; if (edge.adjacent_faces.size() == 2) { diff --git a/model/data/test/TestMeshTopologyDiagnostics.cpp b/model/data/test/TestMeshTopologyDiagnostics.cpp index 64b4ed5..400df69 100644 --- a/model/data/test/TestMeshTopologyDiagnostics.cpp +++ b/model/data/test/TestMeshTopologyDiagnostics.cpp @@ -27,7 +27,7 @@ TEST_CASE("MeshTopologyDiagnostics classifies boundary and dihedral edges") REQUIRE(result.non_manifold_vertices.empty()); } -TEST_CASE("MeshTopologyDiagnostics classifies non manifold edge and vertices") +TEST_CASE("MeshTopologyDiagnostics excludes non manifold edge endpoints from non manifold vertices") { MeshData mesh; mesh.vertex_positions_ = { @@ -42,7 +42,7 @@ TEST_CASE("MeshTopologyDiagnostics classifies non manifold edge and vertices") const MeshTopologyDiagnosticResult result = MeshTopologyDiagnostics::analyze(adjacency, mesh); REQUIRE(result.non_manifold_edges.size() == 1); - REQUIRE(result.non_manifold_vertices.size() == 2); + REQUIRE(result.non_manifold_vertices.empty()); } TEST_CASE("MeshTopologyDiagnostics classifies isolated edge and vertex") -- Gitee From 79c7e23e31dcab563e519f314e520b3e9e73800c Mon Sep 17 00:00:00 2001 From: Young <2109190674@qq.com> Date: Sat, 15 Aug 2026 19:38:35 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix(TopologyDiagnostics):=E4=BA=8C=E9=9D=A2?= =?UTF-8?q?=E8=A7=92=E8=AE=A1=E7=AE=97=E6=94=B9=E4=B8=BA=E5=86=85=E4=BA=8C?= =?UTF-8?q?=E9=9D=A2=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/CentralRenderArea.qml | 4 ++-- model/data/MeshTopologyDiagnostics.cpp | 5 +++-- .../data/test/TestMeshTopologyDiagnostics.cpp | 20 ++++++++++++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/CentralRenderArea.qml b/app/CentralRenderArea.qml index fe705e2..153a5a2 100644 --- a/app/CentralRenderArea.qml +++ b/app/CentralRenderArea.qml @@ -16,8 +16,8 @@ Page { property real faceSelectAngle: 30.0 // 当前渲染区域的拓扑诊断开关和二面角筛选范围。 - property real dihedralMinimumAngle: 30.0 - property real dihedralMaximumAngle: 180.0 + property real dihedralMinimumAngle: 0.0 + property real dihedralMaximumAngle: 150.0 // 标记由功能自动开启的属性渲染,避免影响用户手动打开的属性渲染面板。 property bool featureAttributeRenderingActive: false diff --git a/model/data/MeshTopologyDiagnostics.cpp b/model/data/MeshTopologyDiagnostics.cpp index 23cd0f1..cbf9158 100644 --- a/model/data/MeshTopologyDiagnostics.cpp +++ b/model/data/MeshTopologyDiagnostics.cpp @@ -48,7 +48,7 @@ std::array faceNormal(const MeshData& mesh, Index face) return {}; } -//! @brief 计算两个相邻面单位法向的无符号夹角,范围为 [0, 180] +//! @brief 计算两个相邻面的内二面角:180°减去单位法向夹角,范围为 [0, 180] double dihedralAngle(const std::array& first, const std::array& second) { const double first_length = std::sqrt(first[0] * first[0] + first[1] * first[1] + first[2] * first[2]); @@ -58,7 +58,8 @@ double dihedralAngle(const std::array& first, const std::array