From 2bb8b3a80062d8ca9071dfc4d0f9f275cfb9c4b6 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sat, 25 Jul 2026 19:13:45 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat(plugins):=20=E7=A7=BB=E6=A4=8D=20fea?= =?UTF-8?q?ture/measure-feature-plugin=20=E7=AC=AC=E4=B8=89=E9=83=A8?= =?UTF-8?q?=E5=88=86=EF=BC=88=E6=96=87=E6=9C=AC=E5=A3=B0=E6=98=8E=E4=B8=8E?= =?UTF-8?q?=E6=B5=8B=E9=87=8F=E6=8F=92=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `MeasurePlugin` 功能插件:`MeasureHandler` 提供距离、角度、面积、体积等 测量能力,经 `FeatureRegistrar` 声明参数并注册到 `FeatureSystem` - `FeatureInfo` / `QFeatureInfo` 增加 `execute_text`、`interaction_guide` 文本 声明字段,供通用界面按声明渲染交互功能的 UI 名称与操作说明,不做插件名特判 - 配套 `TestMeasureHandler` 单元测试与 `TestFeatureSystem` 声明字段用例 --- app/model/systems/feature/QFeatureInfo.h | 10 +- .../systems/feature/QFeatureSystemAdaptor.cpp | 4 +- model/systems/feature/FeatureInfo.h | 2 + model/systems/feature/FeatureSystem.cpp | 2 + model/systems/feature/FeatureSystem.h | 2 + .../systems/feature/FeatureSystemRegister.cpp | 2 + .../feature/test/TestFeatureSystem.cpp | 8 +- plugins/feature/CMakeLists.txt | 1 + plugins/feature/MeasurePlugin/CMakeLists.txt | 12 + .../feature/MeasurePlugin/MeasureHandler.cpp | 1169 +++++++++++++++++ .../feature/MeasurePlugin/MeasureHandler.h | 97 ++ plugins/feature/MeasurePlugin/MeasurePlugin.h | 24 + .../feature/MeasurePlugin/MeasurePlugin.json | 11 + .../feature/MeasurePlugin/test/CMakeLists.txt | 2 + .../MeasurePlugin/test/TestMeasureHandler.cpp | 268 ++++ 15 files changed, 1611 insertions(+), 3 deletions(-) create mode 100644 plugins/feature/MeasurePlugin/CMakeLists.txt create mode 100644 plugins/feature/MeasurePlugin/MeasureHandler.cpp create mode 100644 plugins/feature/MeasurePlugin/MeasureHandler.h create mode 100644 plugins/feature/MeasurePlugin/MeasurePlugin.h create mode 100644 plugins/feature/MeasurePlugin/MeasurePlugin.json create mode 100644 plugins/feature/MeasurePlugin/test/CMakeLists.txt create mode 100644 plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp diff --git a/app/model/systems/feature/QFeatureInfo.h b/app/model/systems/feature/QFeatureInfo.h index 2a6932b..f33e615 100644 --- a/app/model/systems/feature/QFeatureInfo.h +++ b/app/model/systems/feature/QFeatureInfo.h @@ -17,9 +17,11 @@ class QFeatureInfo : public QObject { Q_PROPERTY(QString icon READ icon CONSTANT) Q_PROPERTY(QList arg_types READ argTypes CONSTANT) Q_PROPERTY(bool interactive READ interactive CONSTANT) + Q_PROPERTY(QString execute_text READ executeText CONSTANT) + Q_PROPERTY(QString interaction_guide READ interactionGuide CONSTANT) public: QFeatureInfo(QString name, QString display_name, QString description, QString menu_path, QString icon, QList arg_types, - bool interactive = false, QObject* parent = nullptr) + bool interactive = false, QString execute_text = {}, QString interaction_guide = {}, QObject* parent = nullptr) : QObject(parent) , name_(std::move(name)) , display_name_(std::move(display_name)) @@ -28,6 +30,8 @@ public: , icon_(std::move(icon)) , arg_types_(std::move(arg_types)) , interactive_(interactive) + , execute_text_(std::move(execute_text)) + , interaction_guide_(std::move(interaction_guide)) { } QString name() const { return name_; } @@ -37,6 +41,8 @@ public: QString icon() const { return icon_; } QList argTypes() const { return arg_types_; } bool interactive() const { return interactive_; } + QString executeText() const { return execute_text_; } + QString interactionGuide() const { return interaction_guide_; } private: QString name_; //> 功能唯一名称,用作索引 @@ -46,5 +52,7 @@ private: QString icon_; //> 自定义图标的 qrc 资源路径,为空时按插件名映射默认图标 QList arg_types_; //> 功能参数类型列表 bool interactive_ = false; //> 是否声明视口交互能力 + QString execute_text_; //> 参数执行模式的 UI 名称(interactive 功能专用) + QString interaction_guide_; //> 交互模式的操作说明文字(interactive 功能专用) }; #endif // !Q_FEATURE_INFO_H diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.cpp b/app/model/systems/feature/QFeatureSystemAdaptor.cpp index d9a5a25..b6e8e99 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.cpp +++ b/app/model/systems/feature/QFeatureSystemAdaptor.cpp @@ -134,7 +134,9 @@ QList QFeatureSystemAdaptor::getFeaturesInfo() const QString::fromStdString(menu.menu_path.empty() ? "功能" : menu.menu_path), QString::fromStdString(menu.icon), std::move(args), - feature_info->interactive)); + feature_info->interactive, + QString::fromStdString(feature_info->execute_text), + QString::fromStdString(feature_info->interaction_guide))); } } return infos; diff --git a/model/systems/feature/FeatureInfo.h b/model/systems/feature/FeatureInfo.h index 3167484..da80beb 100644 --- a/model/systems/feature/FeatureInfo.h +++ b/model/systems/feature/FeatureInfo.h @@ -35,6 +35,8 @@ struct FeatureInfo { std::string display_name; //> 功能 UI 展示用名称 std::string description; //> 功能描述 bool interactive = false; //> 是否声明视口交互能力(功能经 interaction 上下文订阅交互回调) + std::string execute_text; //> 参数执行模式的 UI 名称(interactive 功能专用,空时 UI 用默认"参数执行") + std::string interaction_guide; //> 交互模式的操作说明文字(interactive 功能专用) std::vector arg_types; //> 功能参数类型列表 std::vector menus; //> 菜单贡献项列表 std::vector key_bindings; //> 按键绑定列表 diff --git a/model/systems/feature/FeatureSystem.cpp b/model/systems/feature/FeatureSystem.cpp index 8331716..78f999d 100644 --- a/model/systems/feature/FeatureSystem.cpp +++ b/model/systems/feature/FeatureSystem.cpp @@ -48,6 +48,8 @@ bool FeatureSystem::registerHandler(const HandlerMetaData& meta_data, SystemHand info->display_name = meta_data.display_name; info->description = meta_data.description; info->interactive = meta_data.interactive; + info->execute_text = meta_data.execute_text; + info->interaction_guide = meta_data.interaction_guide; info->arg_types = registrar.argTypes(); info->menus = registrar.menuItems(); info->key_bindings = registrar.keyBindings(); diff --git a/model/systems/feature/FeatureSystem.h b/model/systems/feature/FeatureSystem.h index 5f50e30..be3266a 100644 --- a/model/systems/feature/FeatureSystem.h +++ b/model/systems/feature/FeatureSystem.h @@ -37,6 +37,8 @@ struct HandlerMetaData { std::string display_name {}; //> 功能 UI 展示用名称 std::string description {}; //> 功能描述 bool interactive {}; //> 是否声明视口交互能力(功能经 interaction 上下文订阅交互回调) + std::string execute_text {}; //> 参数执行模式的 UI 名称(interactive 功能专用,空时 UI 用默认"参数执行") + std::string interaction_guide {}; //> 交互模式的操作说明文字(interactive 功能专用) }; /** diff --git a/model/systems/feature/FeatureSystemRegister.cpp b/model/systems/feature/FeatureSystemRegister.cpp index cf8a642..6e2a163 100644 --- a/model/systems/feature/FeatureSystemRegister.cpp +++ b/model/systems/feature/FeatureSystemRegister.cpp @@ -40,6 +40,8 @@ HandlerMetaData FeatureSystemRegister::toMetaData(const QJsonObject& meta_data) handler_data.display_name = meta_data.value("display_name").toString().toStdString(); handler_data.description = meta_data.value("description").toString().toStdString(); handler_data.interactive = meta_data.value("interactive").toBool(false); + handler_data.execute_text = meta_data.value("execute_text").toString().toStdString(); + handler_data.interaction_guide = meta_data.value("interaction_guide").toString().toStdString(); return handler_data; } } diff --git a/model/systems/feature/test/TestFeatureSystem.cpp b/model/systems/feature/test/TestFeatureSystem.cpp index 7fc29a0..422ac5e 100644 --- a/model/systems/feature/test/TestFeatureSystem.cpp +++ b/model/systems/feature/test/TestFeatureSystem.cpp @@ -244,7 +244,7 @@ TEST_CASE("FeatureSystem re-registering same name replaces old handler", "[Featu REQUIRE(system.getFeatureInfos().size() == 1); } -TEST_CASE("FeatureSystem propagates interactive metadata to FeatureInfo", "[FeatureSystem]") +TEST_CASE("FeatureSystem propagates interactive and text metadata to FeatureInfo", "[FeatureSystem]") { core::EventBus bus; ModelLayer model_layer; @@ -252,12 +252,16 @@ TEST_CASE("FeatureSystem propagates interactive metadata to FeatureInfo", "[Feat auto meta_data = makeMetaData(); meta_data.interactive = true; + meta_data.execute_text = "尺寸标注"; + meta_data.interaction_guide = "点击两点成线"; FeatureSystem::SystemHandlerPtr handler { new FakeFeatureHandler }; REQUIRE(system.registerHandler(meta_data, std::move(handler))); auto infos = system.getFeatureInfos(); REQUIRE(infos.size() == 1); REQUIRE(infos[0]->interactive); + REQUIRE(infos[0]->execute_text == "尺寸标注"); + REQUIRE(infos[0]->interaction_guide == "点击两点成线"); // 未声明时为空串与 false FeatureSystem::SystemHandlerPtr plain { new FakeFeatureHandler }; @@ -269,6 +273,8 @@ TEST_CASE("FeatureSystem propagates interactive metadata to FeatureInfo", "[Feat for (const FeatureInfo* info : infos) { if (info->name == "PlainFeature") { REQUIRE_FALSE(info->interactive); + REQUIRE(info->execute_text.empty()); + REQUIRE(info->interaction_guide.empty()); } } } diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 5a4028f..b678832 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(FeatureDemoPlugin) +add_subdirectory(MeasurePlugin) diff --git a/plugins/feature/MeasurePlugin/CMakeLists.txt b/plugins/feature/MeasurePlugin/CMakeLists.txt new file mode 100644 index 0000000..c2c6568 --- /dev/null +++ b/plugins/feature/MeasurePlugin/CMakeLists.txt @@ -0,0 +1,12 @@ +precess_add_feature_plugin(MeasurePlugin + SOURCES "MeasureHandler.cpp" + PLUGIN_H "MeasurePlugin.h" +) + +precess_plugin_link_libraries(MeasurePlugin + TKernel # Standard / gp 基础类型 + TKBRep # BRep_Tool、BRepAdaptor + TKTopAlgo # BRepGProp、BRepBndLib +) + +add_subdirectory(test) diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.cpp b/plugins/feature/MeasurePlugin/MeasureHandler.cpp new file mode 100644 index 0000000..0ab6d10 --- /dev/null +++ b/plugins/feature/MeasurePlugin/MeasureHandler.cpp @@ -0,0 +1,1169 @@ +/** + * @file MeasureHandler.cpp + * @brief 网格测量处理器,支持距离、角度、半径、长度、面积、体积、包围盒与重心 + * @author 范成通 email 1941804585@qq.com + */ + +#include "MeasureHandler.h" +#include "ArgObject.h" +#include "ComponentData.h" +#include "FeatureContext.h" +#include "FeatureParams.h" +#include "FeatureRegistrar.h" +#include "GeometryData.h" +#include "GeometryRegistry.h" +#include "InteractionContext.h" +#include "MeshData.h" +#include "ModelLayer.h" +#include "Selection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace systems::feature { + +namespace { +using Vec3 = std::array; + +Vec3 operator-(const Vec3& a, const Vec3& b) +{ + return { a[0] - b[0], a[1] - b[1], a[2] - b[2] }; +} + +Vec3 operator+(const Vec3& a, const Vec3& b) +{ + return { a[0] + b[0], a[1] + b[1], a[2] + b[2] }; +} + +Vec3 operator*(const Vec3& a, double s) +{ + return { a[0] * s, a[1] * s, a[2] * s }; +} + +Vec3 operator/(const Vec3& a, double s) +{ + return { a[0] / s, a[1] / s, a[2] / s }; +} + +//! @brief 两点中点 +Vec3 midpoint(const Vec3& a, const Vec3& b) +{ + return { (a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, (a[2] + b[2]) / 2.0 }; +} + +double dot(const Vec3& a, const Vec3& b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +double length(const Vec3& v) +{ + return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); +} + +Vec3 cross(const Vec3& a, const Vec3& b) +{ + return { + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; +} + +enum class MeasureType { + Distance, + Angle, + Radius, + Length, + Area, + Volume, + BoundingBox, + Centroid +}; + +//! @brief 网格测量实现签名 +using MeshExecFn = std::string (*)(const MeshData&, const ModelLayer&, const Selection&); +//! @brief 几何测量实现签名 +using GeomExecFn = std::string (*)(const GeometryRegistry&, const Selection&); + +//! @brief 测量操作表项:名称、选择器模式与两套实现的统一注册处(新增类型只需在 kMeasureOps 加一行) +struct MeasureOp { + MeasureType type; + const char* name; //> Combo 显示名(表内顺序即下标) + const char* mesh_selector_modes; //> 网格组件的选择器模式 + const char* geom_selector_modes; //> 几何组件的选择器模式 + MeshExecFn mesh_exec; //> 网格实现 + GeomExecFn geom_exec; //> 几何实现(nullptr 表示暂不支持几何模型) +}; + +bool isValidVertexId(const MeshData& mesh, const ModelLayer& manager, Index id) +{ + if (id < 0) + return false; + if (mesh.local_to_global_.empty()) + return id < static_cast(mesh.vertex_positions_.size()); + return id < static_cast(manager.globalPoints().size()); +} + +const Vec3& getPosition(const MeshData& mesh, const ModelLayer& manager, Index id) +{ + if (!mesh.local_to_global_.empty() && !manager.globalPoints().empty()) + return manager.globalPoints()[id]; + return mesh.vertex_positions_[id]; +} + +std::string toString(double value, int precision = 6) +{ + std::ostringstream oss; + oss.setf(std::ios::fixed, std::ios::floatfield); + oss.precision(precision); + oss << value; + return oss.str(); +} + +std::string vecString(const Vec3& v) +{ + return "(" + toString(v[0]) + ", " + toString(v[1]) + ", " + toString(v[2]) + ")"; +} + +double angleBetween(const Vec3& u, const Vec3& v) +{ + const double lu = length(u); + const double lv = length(v); + if (lu < std::numeric_limits::epsilon() || lv < std::numeric_limits::epsilon()) + return 0.0; + + double cos_theta = dot(u, v) / (lu * lv); + cos_theta = std::max(-1.0, std::min(1.0, cos_theta)); + return std::acos(cos_theta) * 180.0 / 3.14159265358979323846; +} + +double triangleArea(const Vec3& a, const Vec3& b, const Vec3& c) +{ + return 0.5 * length(cross(b - a, c - a)); +} + +double polygonArea(const std::vector& points) +{ + const size_t n = points.size(); + if (n < 3) + return 0.0; + + double area = 0.0; + for (size_t i = 2; i < n; ++i) + area += triangleArea(points[0], points[i - 1], points[i]); + return area; +} + +double tetraVolume(const Vec3& a, const Vec3& b, const Vec3& c, const Vec3& d) +{ + return std::abs(dot(b - a, cross(c - a, d - a))) / 6.0; +} + +double polyhedronVolume(const MeshData& mesh, const ModelLayer& manager, Index solid_id) +{ + if (solid_id + 1 >= static_cast(mesh.solid_faces_offset_.size())) + return 0.0; + + const Index face_start = mesh.solid_faces_offset_[solid_id]; + const Index face_end = mesh.solid_faces_offset_[solid_id + 1]; + double volume = 0.0; + + for (Index f = face_start; f < face_end; ++f) { + const Index face_id = mesh.solid_faces_[f]; + if (face_id + 1 >= static_cast(mesh.solid_faces_vertices_offset_.size())) + continue; + + const Index vert_start = mesh.solid_faces_vertices_offset_[face_id]; + const Index vert_end = mesh.solid_faces_vertices_offset_[face_id + 1]; + if (vert_end - vert_start < 3) + continue; + + const Vec3& v0 = getPosition(mesh, manager, mesh.solid_faces_vertices_[vert_start]); + for (Index i = vert_start + 1; i + 1 < vert_end; ++i) { + const Vec3& vi = getPosition(mesh, manager, mesh.solid_faces_vertices_[i]); + const Vec3& vj = getPosition(mesh, manager, mesh.solid_faces_vertices_[i + 1]); + volume += dot(v0, cross(vi, vj)) / 6.0; + } + } + + return std::abs(volume); +} + +double solidVolume(const MeshData& mesh, const ModelLayer& manager, Index solid_id) +{ + if (solid_id < 0 || solid_id + 1 >= static_cast(mesh.solid_vertices_offset_.size())) + return 0.0; + if (solid_id >= static_cast(mesh.solid_types_.size())) + return 0.0; + + const unsigned char cell_type = mesh.solid_types_[solid_id]; + const Index start = mesh.solid_vertices_offset_[solid_id]; + const Index end = mesh.solid_vertices_offset_[solid_id + 1]; + + // VTK_TETRA = 10 + if (cell_type == 10) { + if (end - start != 4) + return 0.0; + const Vec3& a = getPosition(mesh, manager, mesh.solid_vertices_[start]); + const Vec3& b = getPosition(mesh, manager, mesh.solid_vertices_[start + 1]); + const Vec3& c = getPosition(mesh, manager, mesh.solid_vertices_[start + 2]); + const Vec3& d = getPosition(mesh, manager, mesh.solid_vertices_[start + 3]); + return tetraVolume(a, b, c, d); + } + + // VTK_POLYHEDRON = 42 + if (cell_type == 42) + return polyhedronVolume(mesh, manager, solid_id); + + return 0.0; +} + +std::vector collectPositions(const MeshData& mesh, const ModelLayer& manager, + ElementEnum::Type type, const std::vector& ids) +{ + std::vector positions; + positions.reserve(ids.size() * 4); + + switch (type) { + case ElementEnum::Vertex: + case ElementEnum::Edge: + // 边选择的 ids 即每条边端点的顶点 id(EdgeSelectorHighlight 约定),与点一样按顶点处理 + for (Index id : ids) { + if (isValidVertexId(mesh, manager, id)) + positions.push_back(getPosition(mesh, manager, id)); + } + break; + case ElementEnum::Face: + for (Index id : ids) { + if (id + 1 >= static_cast(mesh.face_vertices_offset_.size())) + continue; + for (Index i = mesh.face_vertices_offset_[id]; i < mesh.face_vertices_offset_[id + 1]; ++i) { + const Index v = mesh.face_vertices_[i]; + if (isValidVertexId(mesh, manager, v)) + positions.push_back(getPosition(mesh, manager, v)); + } + } + break; + case ElementEnum::Solid: + for (Index id : ids) { + if (id + 1 >= static_cast(mesh.solid_vertices_offset_.size())) + continue; + for (Index i = mesh.solid_vertices_offset_[id]; i < mesh.solid_vertices_offset_[id + 1]; ++i) { + const Index v = mesh.solid_vertices_[i]; + if (isValidVertexId(mesh, manager, v)) + positions.push_back(getPosition(mesh, manager, v)); + } + } + break; + default: + break; + } + + return positions; +} + +std::string formatDistance(const Vec3& a, const Vec3& b) +{ + const Vec3 d = b - a; + std::ostringstream oss; + oss << "Distance: " << toString(length(d)) << "\n" + << "Dx: " << toString(d[0]) << "\n" + << "Dy: " << toString(d[1]) << "\n" + << "Dz: " << toString(d[2]); + return oss.str(); +} + +std::string formatAngle(const Vec3& a, const Vec3& b, const Vec3& c) +{ + std::ostringstream oss; + oss << "Angle: " << toString(angleBetween(a - b, c - b)) << " deg"; + return oss.str(); +} + +std::string formatRadius(const Vec3& a, const Vec3& b, const Vec3& c) +{ + const Vec3 ab = b - a; + const Vec3 ac = c - a; + const Vec3 n = cross(ab, ac); + const double n_len2 = dot(n, n); + + if (n_len2 < std::numeric_limits::epsilon()) + return "错误:三点共线或重合,无法计算半径"; + + const Vec3 circumcenter = a + (cross(ac, n) * dot(ab, ab) - cross(ab, n) * dot(ac, ac)) / (2.0 * n_len2); + const double radius = length(circumcenter - a); + + std::ostringstream oss; + oss << "Radius: " << toString(radius) << "\n" + << "Center: " << vecString(circumcenter); + return oss.str(); +} + +std::string formatEdgeLength(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + // 边选择的 ids 是每条边两个端点的顶点 id 对,逐对计算长度 + for (size_t i = 0; i + 1 < ids.size(); i += 2) { + const Index v0 = ids[i]; + const Index v1 = ids[i + 1]; + const size_t edge_no = i / 2; + if (!isValidVertexId(mesh, manager, v0) || !isValidVertexId(mesh, manager, v1)) { + oss << "边 " << edge_no << ": 无效顶点\n"; + continue; + } + + const double len = length(getPosition(mesh, manager, v1) - getPosition(mesh, manager, v0)); + total += len; + oss << "边 " << edge_no << ": " << toString(len) << "\n"; + } + + oss << "累计长度: " << toString(total); + return oss.str(); +} + +std::string formatFaceArea(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + if (id + 1 >= static_cast(mesh.face_vertices_offset_.size())) { + oss << "面 " << id << ": 无效索引\n"; + continue; + } + + std::vector points; + for (Index i = mesh.face_vertices_offset_[id]; i < mesh.face_vertices_offset_[id + 1]; ++i) + points.push_back(getPosition(mesh, manager, mesh.face_vertices_[i])); + + const double area = polygonArea(points); + total += area; + oss << "面 " << id << ": " << toString(area) << "\n"; + } + + oss << "总面积: " << toString(total); + return oss.str(); +} + +std::string formatSolidVolume(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const double volume = solidVolume(mesh, manager, id); + if (volume <= 0.0) { + oss << "体 " << id << ": 不支持的体类型或无体积\n"; + continue; + } + total += volume; + oss << "体 " << id << ": " << toString(volume) << "\n"; + } + + oss << "总体积: " << toString(total); + return oss.str(); +} + +std::string formatBoundingBox(const std::vector& positions) +{ + // 防御性判空:防止被其他路径以空容器调用时越界访问首元素 + if (positions.empty()) { + spdlog::error("formatBoundingBox: positions 为空"); + return std::string("错误:没有可用的几何数据"); + } + + Vec3 min = positions[0]; + Vec3 max = positions[0]; + + for (const auto& p : positions) { + for (int i = 0; i < 3; ++i) { + min[i] = std::min(min[i], p[i]); + max[i] = std::max(max[i], p[i]); + } + } + + const Vec3 size = max - min; + + std::ostringstream oss; + oss << "包围盒:\n" + << "min: " << vecString(min) << "\n" + << "max: " << vecString(max) << "\n" + << "size: " << vecString(size); + return oss.str(); +} + +std::string formatCentroid(const std::vector& positions) +{ + // 防御性判空:防止空容器时除零产生 NaN + if (positions.empty()) { + spdlog::error("formatCentroid: positions 为空"); + return std::string("错误:没有可用的几何数据"); + } + + Vec3 sum = { 0.0, 0.0, 0.0 }; + for (const auto& p : positions) + sum = sum + p; + const Vec3 centroid = sum / static_cast(positions.size()); + + std::ostringstream oss; + oss << "重心: " << vecString(centroid); + return oss.str(); +} + +// ---------------- 几何(OCC)测量:选择 id 为 GeometryRegistry 分配的全局几何 id ---------------- + +Vec3 toVec3(const gp_Pnt& p) +{ + return { p.X(), p.Y(), p.Z() }; +} + +//! @brief 按选择类型从几何注册表查找子形状,找不到返回 nullptr +const TopoDS_Shape* findGeometryShape(const GeometryRegistry& reg, ElementEnum::Type type, Index id) +{ + switch (type) { + case ElementEnum::GeometryVertex: + return reg.getVertex(id); + case ElementEnum::GeometryEdge: + return reg.getEdge(id); + case ElementEnum::GeometryFace: + return reg.getFace(id); + case ElementEnum::GeometrySolid: + return reg.getSolid(id); + default: + return nullptr; + } +} + +//! @brief 收集选择中有效的几何子形状 +std::vector collectGeometryShapes(const GeometryRegistry& reg, + ElementEnum::Type type, const std::vector& ids) +{ + std::vector shapes; + shapes.reserve(ids.size()); + for (Index id : ids) { + if (const TopoDS_Shape* s = findGeometryShape(reg, type, id)) + shapes.push_back(*s); + } + return shapes; +} + +//! @brief 取几何点坐标 +bool geometryVertexPoint(const GeometryRegistry& reg, Index id, Vec3& out) +{ + const TopoDS_Shape* s = reg.getVertex(id); + if (!s) + return false; + out = toVec3(BRep_Tool::Pnt(TopoDS::Vertex(*s))); + return true; +} + +//! @brief 取几何边中点处的切向作为边方向 +bool geometryEdgeDirection(const TopoDS_Edge& edge, Vec3& out) +{ + BRepAdaptor_Curve curve(edge); + const double u = 0.5 * (curve.FirstParameter() + curve.LastParameter()); + gp_Pnt p; + gp_Vec v; + curve.D1(u, p, v); + if (v.Magnitude() < std::numeric_limits::epsilon()) + return false; + v.Normalize(); + out = { v.X(), v.Y(), v.Z() }; + return true; +} + +std::string formatGeometryEdgeLength(const GeometryRegistry& reg, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const TopoDS_Shape* s = reg.getEdge(id); + if (!s) { + oss << "边 " << id << ": 无效索引\n"; + continue; + } + GProp_GProps props; + BRepGProp::LinearProperties(*s, props); + const double len = props.Mass(); + total += len; + oss << "边 " << id << ": " << toString(len) << "\n"; + } + + oss << "累计长度: " << toString(total); + return oss.str(); +} + +std::string formatGeometryFaceArea(const GeometryRegistry& reg, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const TopoDS_Shape* s = reg.getFace(id); + if (!s) { + oss << "面 " << id << ": 无效索引\n"; + continue; + } + GProp_GProps props; + BRepGProp::SurfaceProperties(*s, props); + const double area = std::abs(props.Mass()); + total += area; + oss << "面 " << id << ": " << toString(area) << "\n"; + } + + oss << "总面积: " << toString(total); + return oss.str(); +} + +std::string formatGeometrySolidVolume(const GeometryRegistry& reg, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const TopoDS_Shape* s = reg.getSolid(id); + if (!s) { + oss << "体 " << id << ": 无效索引\n"; + continue; + } + GProp_GProps props; + BRepGProp::VolumeProperties(*s, props); + const double volume = std::abs(props.Mass()); + total += volume; + oss << "体 " << id << ": " << toString(volume) << "\n"; + } + + oss << "总体积: " << toString(total); + return oss.str(); +} + +std::string formatGeometryBoundingBox(const std::vector& shapes) +{ + Bnd_Box box; + for (const auto& s : shapes) + BRepBndLib::AddOptimal(s, box, false); // 不依赖三角剖分,直接按解析几何求界 + + if (box.IsVoid()) + return std::string("错误:无法计算包围盒"); + + Standard_Real xmin = 0.0, ymin = 0.0, zmin = 0.0, xmax = 0.0, ymax = 0.0, zmax = 0.0; + box.Get(xmin, ymin, zmin, xmax, ymax, zmax); + const Vec3 min { xmin, ymin, zmin }; + const Vec3 max { xmax, ymax, zmax }; + const Vec3 size = max - min; + + std::ostringstream oss; + oss << "包围盒:\n" + << "min: " << vecString(min) << "\n" + << "max: " << vecString(max) << "\n" + << "size: " << vecString(size); + return oss.str(); +} + +//! @brief 按边长 / 面积 / 体积加权的重心 +std::string formatGeometryCentroid(const GeometryRegistry& reg, + ElementEnum::Type type, const std::vector& ids) +{ + double total_mass = 0.0; + Vec3 sum { 0.0, 0.0, 0.0 }; + + for (Index id : ids) { + const TopoDS_Shape* s = findGeometryShape(reg, type, id); + if (!s) + continue; + + GProp_GProps props; + switch (type) { + case ElementEnum::GeometryEdge: + BRepGProp::LinearProperties(*s, props); + break; + case ElementEnum::GeometryFace: + BRepGProp::SurfaceProperties(*s, props); + break; + case ElementEnum::GeometrySolid: + BRepGProp::VolumeProperties(*s, props); + break; + default: + continue; + } + + const double mass = std::abs(props.Mass()); + if (mass < std::numeric_limits::epsilon()) + continue; + sum = sum + toVec3(props.CentreOfMass()) * mass; + total_mass += mass; + } + + if (total_mass < std::numeric_limits::epsilon()) + return std::string("错误:未找到可计算重心的几何对象"); + + std::ostringstream oss; + oss << "重心: " << vecString(sum / total_mass); + return oss.str(); +} + +// ---------------- 几何(OCC)测量实现:选择 id 为 GeometryRegistry 分配的全局几何 id ---------------- + +std::string geomDistance(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometryVertex || selection.ids.size() != 2) { + return std::string("错误:距离测量需要选择两个几何点"); + } + Vec3 a, b; + if (!geometryVertexPoint(reg, selection.ids[0], a) || !geometryVertexPoint(reg, selection.ids[1], b)) + return std::string("错误:无效的几何点 id"); + return formatDistance(a, b); +} + +std::string geomAngle(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type == ElementEnum::GeometryVertex && selection.ids.size() == 3) { + Vec3 a, b, c; + if (!geometryVertexPoint(reg, selection.ids[0], a) + || !geometryVertexPoint(reg, selection.ids[1], b) + || !geometryVertexPoint(reg, selection.ids[2], c)) + return std::string("错误:无效的几何点 id"); + return formatAngle(a, b, c); + } + if (selection.type == ElementEnum::GeometryEdge && selection.ids.size() == 2) { + const TopoDS_Shape* e0 = reg.getEdge(selection.ids[0]); + const TopoDS_Shape* e1 = reg.getEdge(selection.ids[1]); + Vec3 u, v; + if (!e0 || !e1 + || !geometryEdgeDirection(TopoDS::Edge(*e0), u) + || !geometryEdgeDirection(TopoDS::Edge(*e1), v)) + return std::string("错误:无效的几何边 id 或边方向无法计算"); + std::ostringstream oss; + oss << "Angle: " << toString(angleBetween(u, v)) << " deg"; + return oss.str(); + } + return std::string("错误:角度测量需要选择三个几何点或两条几何边"); +} + +std::string geomRadius(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type == ElementEnum::GeometryVertex && selection.ids.size() == 3) { + Vec3 a, b, c; + if (!geometryVertexPoint(reg, selection.ids[0], a) + || !geometryVertexPoint(reg, selection.ids[1], b) + || !geometryVertexPoint(reg, selection.ids[2], c)) + return std::string("错误:无效的几何点 id"); + return formatRadius(a, b, c); + } + if (selection.type == ElementEnum::GeometryEdge && selection.ids.size() == 1) { + const TopoDS_Shape* s = reg.getEdge(selection.ids[0]); + if (!s) + return std::string("错误:无效的几何边 id"); + BRepAdaptor_Curve curve(TopoDS::Edge(*s)); + if (curve.GetType() != GeomAbs_Circle) + return std::string("错误:所选边不是圆弧,无法直接测半径(可改选三个几何点)"); + const gp_Circ circ = curve.Circle(); + std::ostringstream oss; + oss << "Radius: " << toString(circ.Radius()) << "\n" + << "Center: " << vecString(toVec3(circ.Location())); + return oss.str(); + } + return std::string("错误:半径测量需要选择三个几何点或一条圆弧边"); +} + +std::string geomLength(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometryEdge || selection.ids.empty()) { + return std::string("错误:长度测量需要选择几何边"); + } + return formatGeometryEdgeLength(reg, selection.ids); +} + +std::string geomArea(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometryFace || selection.ids.empty()) { + return std::string("错误:面积测量需要选择几何面"); + } + return formatGeometryFaceArea(reg, selection.ids); +} + +std::string geomVolume(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometrySolid || selection.ids.empty()) { + return std::string("错误:体积测量需要选择几何体"); + } + return formatGeometrySolidVolume(reg, selection.ids); +} + +std::string geomBoundingBox(const GeometryRegistry& reg, const Selection& selection) +{ + const auto shapes = collectGeometryShapes(reg, selection.type, selection.ids); + if (shapes.empty()) { + return std::string("错误:未找到有效的几何对象"); + } + return formatGeometryBoundingBox(shapes); +} + +std::string geomCentroid(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type == ElementEnum::GeometryVertex) { + std::vector points; + points.reserve(selection.ids.size()); + for (Index id : selection.ids) { + Vec3 p; + if (geometryVertexPoint(reg, id, p)) + points.push_back(p); + } + if (points.empty()) + return std::string("错误:未找到有效的几何点"); + return formatCentroid(points); + } + if (selection.ids.empty()) { + return std::string("错误:未找到有效的几何对象"); + } + return formatGeometryCentroid(reg, selection.type, selection.ids); +} + +// ---------------- 网格测量实现 ---------------- + +std::string meshDistance(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Vertex || selection.ids.size() != 2) { + spdlog::error("MeasureHandler::execute: 距离测量需要选择两个点"); + return std::string("错误:距离测量需要选择两个点"); + } + const Vec3& a = getPosition(mesh, manager, selection.ids[0]); + const Vec3& b = getPosition(mesh, manager, selection.ids[1]); + return formatDistance(a, b); +} + +std::string meshAngle(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type == ElementEnum::Vertex && selection.ids.size() == 3) { + const Vec3& a = getPosition(mesh, manager, selection.ids[0]); + const Vec3& b = getPosition(mesh, manager, selection.ids[1]); + const Vec3& c = getPosition(mesh, manager, selection.ids[2]); + return formatAngle(a, b, c); + } + if (selection.type == ElementEnum::Edge && selection.ids.size() == 4) { + // 边选择的 ids 是每条边两个端点的顶点 id(EdgeSelectorHighlight 约定) + const auto& ids = selection.ids; + const bool valid = isValidVertexId(mesh, manager, ids[0]) && isValidVertexId(mesh, manager, ids[1]) + && isValidVertexId(mesh, manager, ids[2]) && isValidVertexId(mesh, manager, ids[3]); + if (valid) { + const Vec3 u = getPosition(mesh, manager, ids[1]) - getPosition(mesh, manager, ids[0]); + const Vec3 v = getPosition(mesh, manager, ids[3]) - getPosition(mesh, manager, ids[2]); + std::ostringstream oss; + oss << "Angle: " << toString(angleBetween(u, v)) << " deg"; + return oss.str(); + } + } + spdlog::error("MeasureHandler::execute: 角度测量需要选择三个点或两条边"); + return std::string("错误:角度测量需要选择三个点或两条边"); +} + +std::string meshRadius(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Vertex || selection.ids.size() != 3) { + spdlog::error("MeasureHandler::execute: 半径测量需要选择三个点"); + return std::string("错误:半径测量需要选择三个点"); + } + const Vec3& a = getPosition(mesh, manager, selection.ids[0]); + const Vec3& b = getPosition(mesh, manager, selection.ids[1]); + const Vec3& c = getPosition(mesh, manager, selection.ids[2]); + return formatRadius(a, b, c); +} + +std::string meshLength(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + // 边选择的 ids 是每条边两个端点的顶点 id 对,数量应为正偶数 + if (selection.type != ElementEnum::Edge || selection.ids.empty() || selection.ids.size() % 2 != 0) { + spdlog::error("MeasureHandler::execute: 长度测量需要选择边"); + return std::string("错误:长度测量需要选择边"); + } + return formatEdgeLength(mesh, manager, selection.ids); +} + +std::string meshArea(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Face) { + spdlog::error("MeasureHandler::execute: 面积测量需要选择面"); + return std::string("错误:面积测量需要选择面"); + } + return formatFaceArea(mesh, manager, selection.ids); +} + +std::string meshVolume(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Solid) { + spdlog::error("MeasureHandler::execute: 体积测量需要选择体"); + return std::string("错误:体积测量需要选择体"); + } + return formatSolidVolume(mesh, manager, selection.ids); +} + +std::string meshBoundingBox(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + const auto positions = collectPositions(mesh, manager, selection.type, selection.ids); + if (positions.empty()) { + spdlog::error("MeasureHandler::execute: 未找到可用顶点"); + return std::string("错误:未找到可用顶点"); + } + return formatBoundingBox(positions); +} + +std::string meshCentroid(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + const auto positions = collectPositions(mesh, manager, selection.type, selection.ids); + if (positions.empty()) { + spdlog::error("MeasureHandler::execute: 未找到可用顶点"); + return std::string("错误:未找到可用顶点"); + } + return formatCentroid(positions); +} + +//! @brief 测量操作注册表:新增测量类型只需在此加一行,args_type/校验/分发全部由表生成 +const MeasureOp kMeasureOps[] = { + { MeasureType::Distance, "距离", "Vertex", "GeometryVertex", meshDistance, geomDistance }, + { MeasureType::Angle, "角度", "Vertex,Edge", "GeometryVertex,GeometryEdge", meshAngle, geomAngle }, + { MeasureType::Radius, "半径", "Vertex", "GeometryVertex,GeometryEdge", meshRadius, geomRadius }, + { MeasureType::Length, "长度", "Edge", "GeometryEdge", meshLength, geomLength }, + { MeasureType::Area, "面积", "Face", "GeometryFace", meshArea, geomArea }, + { MeasureType::Volume, "体积", "Solid", "GeometrySolid", meshVolume, geomVolume }, + { MeasureType::BoundingBox, "包围盒", "Vertex,Edge,Face,Solid", "GeometryVertex,GeometryEdge,GeometryFace,GeometrySolid", meshBoundingBox, geomBoundingBox }, + { MeasureType::Centroid, "重心", "Vertex,Edge,Face,Solid", "GeometryVertex,GeometryEdge,GeometryFace,GeometrySolid", meshCentroid, geomCentroid }, +}; + +//! @brief 按下标查注册表,越界返回 nullptr +const MeasureOp* findMeasureOp(int index) +{ + if (index < 0 || index >= static_cast(sizeof(kMeasureOps) / sizeof(kMeasureOps[0]))) + return nullptr; + return &kMeasureOps[index]; +} + +//! @brief 由注册表生成 Combo 内容(名称与下标天然一致) +std::string comboContent() +{ + std::string out; + for (const MeasureOp& op : kMeasureOps) { + if (!out.empty()) + out += ","; + out += op.name; + } + return out; +} + +//! @brief 由注册表生成选择器内容:网格模式按 Vertex,Edge,Face,Solid 规范序取并集 +std::string selectorContent() +{ + static const char* canonical[] = { "Vertex", "Edge", "Face", "Solid" }; + std::string out; + for (const char* mode : canonical) { + bool used = false; + for (const MeasureOp& op : kMeasureOps) { + if (std::string(op.mesh_selector_modes).find(mode) != std::string::npos) { + used = true; + break; + } + } + if (used) { + if (!out.empty()) + out += ","; + out += mode; + } + } + return out; +} +} + +void MeasureHandler::setup(FeatureRegistrar& reg) +{ + assertExecuteThread(); + // Combo 内容全部由测量操作注册表生成,名称与下标天然一致 + reg.addParameter({ ArgTypeEnum::Combo, "测量类型", comboContent() + "|0" }); + reg.addParameter({ ArgTypeEnum::Selector, "选择对象", selectorContent() }); + reg.addMenuItem({ "工具", "测量" }); +} + +std::any MeasureHandler::execute(FeatureContext& ctx) +{ + assertExecuteThread(); + + const int* type_index = ctx.params.value(0).get(); + if (!type_index) { + spdlog::error("MeasureHandler::execute: 测量类型参数无效"); + return std::string("错误:测量类型参数无效"); + } + + const auto selection_ptr = ctx.params.value(1).get(); + if (!selection_ptr || !*selection_ptr) { + spdlog::error("MeasureHandler::execute: 选择对象参数无效"); + return std::string("错误:选择对象参数无效"); + } + + const auto& selection = **selection_ptr; + + // 选择集未带组件 id 时回退当前活动组件 + Index selected_component_id = selection.component_id; + if (selected_component_id < 0) { + const auto active_component = ctx.activeComponent ? ctx.activeComponent() : std::nullopt; + if (active_component) { + selected_component_id = *active_component; + } + } + ComponentData* comp = ctx.model.findComponent(selected_component_id); + if (!comp) { + spdlog::error("MeasureHandler::execute: 找不到选择对象所在的组件"); + return std::string("错误:找不到选择对象所在的组件"); + } + ModelLayer& manager = ctx.model; + const MeasureOp* op = findMeasureOp(*type_index); + if (!op) { + spdlog::error("MeasureHandler::execute: 未知测量类型下标 {}", *type_index); + return std::string("错误:未知测量类型"); + } + + MeshData* mesh = comp->asMeshData(); + if (!mesh) { + // 无网格但有几何(STEP/IGES):走 OCC 几何测量 + if (comp->geometry) { + comp->geometry->ensureIndexBuilt(manager.geomRegistry()); + if (!op->geom_exec) + return std::string("错误:") + op->name + "测量暂不支持几何模型"; + return op->geom_exec(manager.geomRegistry(), selection); + } + spdlog::error("MeasureHandler::execute: 选择对象所在组件没有网格或几何数据"); + return std::string("错误:选择对象所在组件没有网格或几何数据"); + } + + return op->mesh_exec(*mesh, manager, selection); +} + +void MeasureHandler::activate(FeatureContext& ctx) +{ + assertExecuteThread(); + ctx.interaction.onActivate([this]() { this->clear(); }); + ctx.interaction.onDeactivate([this]() { this->clear(); }); + ctx.interaction.onPick([this](const PickInfo& p) { return this->onPick(p); }); + ctx.interaction.onHover([this](const PickInfo& p) { return this->onHover(p); }); +} +// ---------------- 交互回调(经 InteractionContext 注册) ---------------- + +namespace { +constexpr double kEps = 1e-9; +} + +void MeasureHandler::assertExecuteThread() const +{ + assert(std::this_thread::get_id() == gui_thread_id_); +} + +void MeasureHandler::assertInteractiveThread() const +{ + if (interactive_thread_id_ == std::thread::id()) + interactive_thread_id_ = std::this_thread::get_id(); + assert(std::this_thread::get_id() == interactive_thread_id_); +} + +bool MeasureHandler::samePoint(const PickInfo& a, const PickInfo& b) +{ + if (a.mesh_id >= 0 && a.mesh_id == b.mesh_id) + return true; + if (a.geom_id >= 0 && a.geom_id == b.geom_id) + return true; + // 同源顶点再次拾取坐标位级一致,作无 id 时的兜底 + return a.world_pos == b.world_pos; +} + +bool MeasureHandler::onPick(const PickInfo& pick) +{ + assertInteractiveThread(); + if (!pick.valid) + return false; + + if (!pending_) { + pending_ = pick; + } else if (samePoint(*pending_, pick)) { + pending_.reset(); // 同一点再点一次 = 取消本次起笔 + } else { + addLine(*pending_, pick); + pending_.reset(); + } + + refreshAnnotations(); + return true; +} + +bool MeasureHandler::onHover(const PickInfo& pick) +{ + assertInteractiveThread(); + // 未吸附或无起笔:清除已有预览;本来无预览则无需刷新 + if (!pending_ || !pick.valid) { + if (!has_preview_) + return false; + has_preview_ = false; + refreshAnnotations(); + return true; + } + + has_preview_ = true; + preview_ = pick; + refreshAnnotations(); + return true; +} + +void MeasureHandler::addLine(const PickInfo& a, const PickInfo& b) +{ + // 与已有线完全重复(含反向)则忽略 + for (const MeasureLine& l : lines_) { + if ((samePoint(l.a, a) && samePoint(l.b, b)) || (samePoint(l.a, b) && samePoint(l.b, a))) + return; + } + + // 与每条已有线做端点匹配,共端点即记录一组夹角 + const int new_idx = static_cast(lines_.size()); + for (size_t i = 0; i < lines_.size(); ++i) { + const MeasureLine& l = lines_[i]; + auto try_share = [&](const PickInfo& old_shared, const PickInfo& old_other, + const PickInfo& new_shared, const PickInfo& new_other) { + if (!samePoint(old_shared, new_shared)) + return; + MeasureAngle ang; + ang.line1 = static_cast(i); + ang.line2 = new_idx; + ang.at = old_shared.world_pos; + ang.p = old_other.world_pos; + ang.q = new_other.world_pos; + ang.angle = angleBetween(ang.p - ang.at, ang.q - ang.at); + angles_.push_back(ang); + }; + try_share(l.a, l.b, a, b); + try_share(l.a, l.b, b, a); + try_share(l.b, l.a, a, b); + try_share(l.b, l.a, b, a); + } + lines_.push_back({ a, b }); +} + +void MeasureHandler::refreshAnnotations() +{ + annotations_.clear(); + + // 已确认线的端点与线段(红色端点、绿色实线) + for (const MeasureLine& l : lines_) { + annotations_.points.push_back({ l.a.world_pos }); + annotations_.points.push_back({ l.b.world_pos }); + annotations_.lines.push_back({ l.a.world_pos, l.b.world_pos }); + } + if (pending_) + annotations_.points.push_back({ pending_->world_pos }); + + // 长度文本:每线一个,放线段中点(白色) + for (const MeasureLine& l : lines_) { + const Vec3 mid = midpoint(l.a.world_pos, l.b.world_pos); + annotations_.texts.push_back({ mid, "L: " + toString(length(l.b.world_pos - l.a.world_pos), 2), 1.0, 1.0, 1.0 }); + } + + // 夹角文本:放共点沿角平分线偏移(青色);同一点多个夹角按序号加大偏移防重叠 + for (size_t ai = 0; ai < angles_.size(); ++ai) { + const MeasureAngle& ang = angles_[ai]; + int stack = 0; + for (size_t j = 0; j < ai; ++j) { + if (angles_[j].at == ang.at) + ++stack; + } + + const Vec3 u = ang.p - ang.at; + const Vec3 v = ang.q - ang.at; + const double lu = length(u); + const double lv = length(v); + Vec3 dir { 0.0, 0.0, 0.0 }; + if (lu > kEps && lv > kEps) { + // 角平分线方向;u、v 近反向(180°)时退化为两端点中点方向 + const Vec3 s { u[0] / lu + v[0] / lv, u[1] / lu + v[1] / lv, u[2] / lu + v[2] / lv }; + const double ls = length(s); + if (ls > kEps) { + dir = { s[0] / ls, s[1] / ls, s[2] / ls }; + } else { + const Vec3 d = midpoint(ang.p, ang.q) - ang.at; + const double ld = length(d); + dir = ld > kEps ? Vec3 { d[0] / ld, d[1] / ld, d[2] / ld } + : Vec3 { u[0] / lu, u[1] / lu, u[2] / lu }; + } + } + const double dist = 0.25 * std::min(lu, lv) * (1.0 + 0.3 * stack); + annotations_.texts.push_back({ { ang.at[0] + dir[0] * dist, ang.at[1] + dir[1] * dist, + ang.at[2] + dir[2] * dist }, + "Ang: " + toString(ang.angle, 2), 0.3, 0.9, 1.0 }); + } + + // 悬停动态预览:黄色虚线 + 黄色长度文本 + if (pending_ && has_preview_) { + AnnotationLine preview; + preview.p0 = pending_->world_pos; + preview.p1 = preview_.world_pos; + preview.r = 1.0; + preview.g = 0.9; + preview.b = 0.1; + preview.dashed = true; + annotations_.lines.push_back(preview); + + const Vec3 mid = midpoint(pending_->world_pos, preview_.world_pos); + annotations_.texts.push_back({ mid, + "L: " + toString(length(preview_.world_pos - pending_->world_pos), 2), 1.0, 0.9, 0.1 }); + } +} + +std::string MeasureHandler::resultText() const +{ + assertInteractiveThread(); + if (lines_.empty() && !pending_) + return {}; + + std::string out = "已完成直线: " + std::to_string(lines_.size()) + " 条"; + if (pending_) + out += "(已选第 1 点,再点第 2 点成线)"; + for (size_t i = 0; i < lines_.size(); ++i) { + out += "\nL" + std::to_string(i + 1) + ": " + + toString(length(lines_[i].b.world_pos - lines_[i].a.world_pos), 6); + } + for (const MeasureAngle& ang : angles_) { + out += "\n夹角(L" + std::to_string(ang.line1 + 1) + ",L" + std::to_string(ang.line2 + 1) + + "): " + toString(ang.angle, 6); + } + return out; +} + +void MeasureHandler::clear() +{ + assertInteractiveThread(); + pending_.reset(); + has_preview_ = false; + lines_.clear(); + angles_.clear(); + refreshAnnotations(); +} + +} // namespace systems::feature diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.h b/plugins/feature/MeasurePlugin/MeasureHandler.h new file mode 100644 index 0000000..04d115f --- /dev/null +++ b/plugins/feature/MeasurePlugin/MeasureHandler.h @@ -0,0 +1,97 @@ +/** + * @file MeasureHandler.h + * @brief 测量功能处理器声明:距离、角度、半径、长度、面积、体积、包围盒与重心 + * @author 范成通 email 1941804585@qq.com + */ +#pragma once +#include "FeatureHandler.h" +#include "InteractiveTypes.h" +#include +#include +#include +#include + +namespace systems::feature { + +using systems::interaction::AnnotationBatch; +using systems::interaction::AnnotationLine; +using systems::interaction::PickInfo; + +/** + * @brief 测量功能处理器:尺寸标注(FeatureHandler)+ 交互测量(经 InteractionContext 注册回调) + * + * 尺寸标注:功能参数(测量类型 + 选择对象)+ 菜单执行 → 返回结果文本(GUI 线程调用)。 + * 交互测量:两点成线、共端点两线自动标注夹角(渲染线程经 InteractionService 驱动)。 + * + * @par 线程模型与共享约束(修改前必读) + * - setup()/execute() 由 **GUI 线程**调用(FeatureSystem 注册与菜单触发)。 + * - onPick/onHover/annotations/clear 由 **渲染线程**调用 + * (InteractionService 经 dispatch_async 驱动;本类在 activate() 中经 ctx.interaction 注册回调)。 + * - 因此约定:**setup()/execute() 不得读写交互状态成员** + * (pending_、has_preview_、preview_、lines_、angles_、annotations_); + * **交互事件方法不得触碰标注执行路径的状态**(execute 当前是无状态的,新增状态前必须评审)。 + * - 本类**无锁**。UI 层的模式互斥(交互模式隐藏执行按钮、参数模式停止交互)只是当前的安全网, + * 不是设计依据;确需跨两侧共享状态时,先加锁或经设计评审,不得直接访问。 + * 上述线程模型由 assertExecuteThread()/assertInteractiveThread() 在调试期以 assert 强制。 + */ +class MeasureHandler : public FeatureHandler { +public: + MeasureHandler() = default; + ~MeasureHandler() override = default; + + //! @brief 声明功能参数(测量类型、选择对象)与菜单项 + void setup(FeatureRegistrar& reg) override; + //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用) + void activate(FeatureContext& ctx) override; + //! @brief 尺寸标注:按参数中的测量类型与选择对象执行,返回结果文本 + std::any execute(FeatureContext& ctx) override; + + //! @brief 交互测量状态查询(测试与面板用) + int lineCount() const { return static_cast(lines_.size()); } + bool hasPending() const { return pending_.has_value(); } + +private: + // ---- 交互回调(原 InteractiveHandler 接口方法,现由 activate() 注册到 InteractionContext) ---- + bool onPick(const PickInfo& pick); + bool onHover(const PickInfo& pick); + const AnnotationBatch& annotations() const { assertInteractiveThread(); return annotations_; } + std::string resultText() const; + void clear(); + + //! @brief 交互测量线:两个吸附点(PickInfo 已含世界坐标与两套顶点 id) + struct MeasureLine { + PickInfo a, b; + }; + //! @brief 共端点两线的夹角:at 为共点,p/q 为两线各自另一端点 + struct MeasureAngle { + int line1 = 0; //> lines_ 下标 + int line2 = 0; + std::array at; + std::array p; + std::array q; + double angle = 0.0; + }; + + static bool samePoint(const PickInfo& a, const PickInfo& b); + void addLine(const PickInfo& a, const PickInfo& b); + //! @brief 由当前状态重建标注集 + void refreshAnnotations(); + + //! @brief 线程亲和守卫(仅调试期生效):setup/execute 必须在构造线程(GUI)调用 + void assertExecuteThread() const; + //! @brief 线程亲和守卫(仅调试期生效):交互方法必须始终在单一线程上调用 + //! (应用内为渲染线程;测试单线程环境下与构造线程相同也合法) + void assertInteractiveThread() const; + + std::optional pending_; //> 已起笔未成线的首点 + bool has_preview_ = false; //> 悬停是否正在预览 + PickInfo preview_ {}; //> 悬停预览吸附点 + // 以下交互状态成员仅允许渲染线程的交互方法访问(见类注释的线程模型约定) + std::vector lines_; + std::vector angles_; + AnnotationBatch annotations_; + + const std::thread::id gui_thread_id_ = std::this_thread::get_id(); //> 构造线程(GUI 线程)id + mutable std::thread::id interactive_thread_id_ {}; //> 首个交互调用所在线程 id +}; +} diff --git a/plugins/feature/MeasurePlugin/MeasurePlugin.h b/plugins/feature/MeasurePlugin/MeasurePlugin.h new file mode 100644 index 0000000..381b20d --- /dev/null +++ b/plugins/feature/MeasurePlugin/MeasurePlugin.h @@ -0,0 +1,24 @@ +/** + * @file MeasurePlugin.h + * @brief 测量插件声明:将 MeasureHandler 注册到功能系统 + * @author 范成通 email 1941804585@qq.com + */ +#pragma once +#include "HandlerCreatorDestroyerFactory.h" +#include "MeasureHandler.h" +#include "PluginBase.h" + +#include + +namespace systems::feature { +class MeasurePlugin : public QObject, public PluginBase { + Q_OBJECT + Q_INTERFACES(systems::PluginBase) + Q_PLUGIN_METADATA(IID "com.PreCess.systems.feature.MeasurePlugin/1.0" FILE "MeasurePlugin.json") +private: + const HandlerCreatorDestroyer& getHandlerCreatorDestroyer() noexcept override final + { + return HandlerCreatorDestroyerFactory::get(); + } +}; +} diff --git a/plugins/feature/MeasurePlugin/MeasurePlugin.json b/plugins/feature/MeasurePlugin/MeasurePlugin.json new file mode 100644 index 0000000..e9f009f --- /dev/null +++ b/plugins/feature/MeasurePlugin/MeasurePlugin.json @@ -0,0 +1,11 @@ +{ + "system": "FeatureSystem", + "handler": { + "name": "MeasurePlugin", + "display_name": "测量", + "description": "交互式测量与尺寸标注:两点成线、共点夹角、距离/角度/半径/长度/面积/体积/包围盒/重心", + "interactive": true, + "execute_text": "尺寸标注", + "interaction_guide": "依次点击拾取两点构成一条直线并显示长度;可连续画多条线,两线共端点时自动显示夹角;同一点连点两次取消当前起笔。" + } +} diff --git a/plugins/feature/MeasurePlugin/test/CMakeLists.txt b/plugins/feature/MeasurePlugin/test/CMakeLists.txt new file mode 100644 index 0000000..595471c --- /dev/null +++ b/plugins/feature/MeasurePlugin/test/CMakeLists.txt @@ -0,0 +1,2 @@ +precess_add_test(TestMeasureHandler TestMeasureHandler.cpp) +precess_test_link_libraries(TestMeasureHandler MeasurePluginlib DataTest TKPrim TKBRep TKTopAlgo TKernel) diff --git a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp new file mode 100644 index 0000000..d9f80b0 --- /dev/null +++ b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp @@ -0,0 +1,268 @@ +/** + * @file TestMeasureHandler.cpp + * @brief 测量功能处理器的单元测试 + * @author 范成通 email 1941804585@qq.com + */ + +#include "MeasureHandler.h" +#include "ArgObject.h" +#include "ComponentData.h" +#include "EventBus.h" +#include "FeatureContext.h" +#include "FeatureParams.h" +#include "FeatureRegistrar.h" +#include "GeometryData.h" +#include "InteractionContext.h" +#include "InteractionState.h" +#include "MakeMeshData.h" +#include "MeshData.h" +#include "ModelLayer.h" +#include "Selection.h" + +#include +#include + +#include + +#include +#include +#include +#include + +using namespace systems::feature; + +namespace { + +std::shared_ptr makeVertexSelection(const std::vector& ids) +{ + return std::make_shared(Selection { ids, ElementEnum::Vertex, 0 }); +} + +std::shared_ptr makeEdgeSelection(const std::vector& ids) +{ + return std::make_shared(Selection { ids, ElementEnum::Edge, 0 }); +} + +std::shared_ptr makeGeometrySelection(ElementEnum::Type type, const std::vector& ids) +{ + return std::make_shared(Selection { ids, type, 0 }); +} + +//! @brief 功能测试环境:手工装配 FeatureContext(参数集 + 活动组件 provider),替代原算法 HandlerContext +struct FeatureTestEnv { + ModelLayer mgr; + core::EventBus bus; + MeasureHandler handler; + FeatureRegistrar registrar; + std::optional active_component; + std::unique_ptr params; + systems::interaction::InteractionState interaction_state_; + InteractionContext interaction_ctx_; + std::unique_ptr ctx; + + FeatureTestEnv() + : interaction_ctx_(interaction_state_) + { + // 参数声明直接取自被测功能的 setup,避免测试中重复维护一份 + handler.setup(registrar); + params = std::make_unique(registrar.argTypes()); + ctx = std::make_unique(FeatureContext { + mgr, + bus, + *params, + interaction_ctx_, + []() -> std::optional { return std::nullopt; }, + [this]() { return active_component; }, + [this](Index component_id) { return mgr.getComponentOperator(component_id); }, + }); + } + + //! @brief 写入功能参数并执行尺寸标注,返回结果字符串 + std::string executeMeasure(int measure_type_index, std::shared_ptr selection) + { + params->setValue(0, core::ArgObject::create(measure_type_index)); + params->setValue(1, core::ArgObject::create(std::move(selection))); + auto result_any = handler.execute(*ctx); + if (const std::string* result = std::any_cast(&result_any)) + return *result; + return {}; + } +}; + +//! @brief 在 10×10×10 的 OCC 立方体几何组件上执行一次测量,返回结果字符串 +std::string executeGeometryMeasureOnBox(int measure_type_index, ElementEnum::Type sel_type, int local_id) +{ + BRepPrimAPI_MakeBox box_maker(10.0, 10.0, 10.0); + box_maker.Build(); + if (!box_maker.IsDone()) + return {}; + + auto geometry = std::make_unique(); + geometry->rootShape = std::make_unique(box_maker.Shape()); + + auto c = std::make_unique(); + c->id = -1; + c->name = "box"; + c->geometry = std::move(geometry); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("box", std::move(comps)); + auto cids = env.mgr.modelById(model_id)->componentIds(); + if (cids.size() != 1) + return {}; + + ComponentData* comp = env.mgr.findComponent(cids[0]); + if (!comp) + return {}; + comp->geometry->ensureIndexBuilt(env.mgr.geomRegistry()); + + Index gid = -1; + switch (sel_type) { + case ElementEnum::GeometryEdge: + gid = comp->geometry->index.edgeGlobalId(local_id); + break; + case ElementEnum::GeometryFace: + gid = comp->geometry->index.faceGlobalId(local_id); + break; + case ElementEnum::GeometrySolid: + gid = comp->geometry->index.solidGlobalId(local_id); + break; + default: + break; + } + + auto selection = makeGeometrySelection(sel_type, { gid }); + selection->component_id = cids[0]; + return env.executeMeasure(measure_type_index, selection); +} + +} // namespace + +TEST_CASE("MeasureHandler setup declares measure parameters and menu") +{ + MeasureHandler handler; + FeatureRegistrar reg; + handler.setup(reg); + + REQUIRE(reg.argTypes().size() == 2); + CHECK(reg.argTypes()[0].type == ArgTypeEnum::Combo); + CHECK(reg.argTypes()[1].type == ArgTypeEnum::Selector); + REQUIRE(reg.menuItems().size() == 1); +} + +TEST_CASE("MeasureHandler: distance between two vertices") +{ + auto mesh = std::make_unique(MakeMeshData()); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("test", std::move(comps)); + + auto cids = env.mgr.modelById(model_id)->componentIds(); + REQUIRE(cids.size() == 1); + + ComponentData* comp = env.mgr.findComponent(cids[0]); + REQUIRE(comp); + + MeshData* mesh_ptr = comp->asMeshData(); + REQUIRE(mesh_ptr); + const Index base = mesh_ptr->local_to_global_[0]; + + auto selection = makeVertexSelection({ base + 0, base + 1 }); + selection->component_id = cids[0]; + const std::string result = env.executeMeasure(0, selection); // 距离 + CHECK(result.find("Distance") != std::string::npos); + CHECK(result.find("1.000000") != std::string::npos); +} + +TEST_CASE("MeasureHandler: length of edges selected as endpoint vertex id pairs") +{ + auto mesh = std::make_unique(MakeMeshData()); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("test", std::move(comps)); + + auto cids = env.mgr.modelById(model_id)->componentIds(); + REQUIRE(cids.size() == 1); + + ComponentData* comp = env.mgr.findComponent(cids[0]); + REQUIRE(comp); + + MeshData* mesh_ptr = comp->asMeshData(); + REQUIRE(mesh_ptr); + const Index base = mesh_ptr->local_to_global_[0]; + + // 边选择的 ids 是端点顶点 id 对:{12,13} 与 {0,1} 长度均为 1 + auto selection = makeEdgeSelection({ base + 12, base + 13, base + 0, base + 1 }); + selection->component_id = cids[0]; + const std::string result = env.executeMeasure(3, selection); // 长度 + CHECK(result.find("累计长度: 2.000000") != std::string::npos); +} + +TEST_CASE("MeasureHandler: angle between two edges selected as endpoint vertex id pairs") +{ + auto mesh = std::make_unique(MakeMeshData()); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("test", std::move(comps)); + + auto cids = env.mgr.modelById(model_id)->componentIds(); + REQUIRE(cids.size() == 1); + + ComponentData* comp = env.mgr.findComponent(cids[0]); + REQUIRE(comp); + + MeshData* mesh_ptr = comp->asMeshData(); + REQUIRE(mesh_ptr); + const Index base = mesh_ptr->local_to_global_[0]; + + // 边 {0,1} 方向 (1,0,0),边 {0,3} 方向 (0,1,0),夹角 90° + auto selection = makeEdgeSelection({ base + 0, base + 1, base + 0, base + 3 }); + selection->component_id = cids[0]; + const std::string result = env.executeMeasure(1, selection); // 角度 + CHECK(result.find("Angle: 90.000000 deg") != std::string::npos); +} + +TEST_CASE("MeasureHandler: geometry edge length on OCC box") +{ + // 10×10×10 立方体任一条几何边长度均为 10 + const std::string result = executeGeometryMeasureOnBox(3, ElementEnum::GeometryEdge, 1); + CHECK(result.find("累计长度: 10.000000") != std::string::npos); +} + +TEST_CASE("MeasureHandler: geometry face area on OCC box") +{ + // 10×10×10 立方体任一个几何面面积均为 100 + const std::string result = executeGeometryMeasureOnBox(4, ElementEnum::GeometryFace, 1); + CHECK(result.find("总面积: 100.000000") != std::string::npos); +} + +TEST_CASE("MeasureHandler: geometry solid volume on OCC box") +{ + // 10×10×10 立方体体积为 1000 + const std::string result = executeGeometryMeasureOnBox(5, ElementEnum::GeometrySolid, 1); + CHECK(result.find("总体积: 1000.000000") != std::string::npos); +} -- Gitee From a403eb62cd08b657dc041625d22429a98ce58efb Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sat, 25 Jul 2026 20:56:07 +0800 Subject: [PATCH 02/13] =?UTF-8?q?feat(systems,app,plugins):=20=E6=8E=A5?= =?UTF-8?q?=E9=80=9A=E6=B5=8B=E9=87=8F=E6=8F=92=E4=BB=B6=E8=A7=86=E5=8F=A3?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=EF=BC=9A=E5=8D=95=E6=BF=80=E6=B4=BB=E5=90=AF?= =?UTF-8?q?=E5=81=9C=E9=93=BE=E4=B8=8E=E4=BE=A7=E8=BE=B9=E6=A0=8F=E5=8F=8C?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `FeatureSystem` 新增 `setInteractionActive(name, active)`:单激活、幂等启停, 非 interactive 或未注册的功能返回 false;`InteractionContext` / `InteractionState` 增加 `on_clear` 回调,供"清除"语义从渲染层回溯到功能 - app 层补齐接线:`QFeatureSystemAdaptor::setInteractionActive` 供 QML 调用; `InteractionService::clear()` 清空标注后刷新;`QRenderWindow::clearInteraction()` - `SideBar.qml` 交互/参数双模式:声明 `interactive` 的功能显示模式切换, 交互模式下隐藏参数列表与执行按钮、提供"清除"按钮,启停经 `applyInteractionMode()` 幂等应用,切换功能时先下线旧功能再激活新功能 - `MeasureHandler` 精简为交互测量:参数仅保留"测量类型"与"选择对象",拾取 结果经 `AnnotationBatch` 写入 `InteractionState` 标注;修复标注写入自持成员 导致渲染层拉空标注的问题,改为持有 `ctx.interaction.annotations()` 指针 - 测试:`TestFeatureSystem` 增加 `setInteractionActive` 用例; `TestMeasureHandler` 断言标注写入与 `on_clear` 清空;恢复 `TestInteractionService` 的 clear 覆盖并修正测试自身时序 --- app/SideBar.qml | 79 ++++++++++++++++--- .../systems/feature/QFeatureSystemAdaptor.cpp | 5 ++ .../systems/feature/QFeatureSystemAdaptor.h | 6 ++ app/render/InteractionService.cpp | 9 +++ app/render/InteractionService.h | 2 + app/render/QRenderWindow.cpp | 7 ++ app/render/QRenderWindow.h | 5 ++ app/render/test/TestInteractionService.cpp | 11 ++- model/systems/core/InteractionState.h | 1 + model/systems/feature/FeatureSystem.cpp | 11 +++ model/systems/feature/FeatureSystem.h | 8 ++ model/systems/feature/InteractionContext.cpp | 5 ++ model/systems/feature/InteractionContext.h | 2 + .../feature/test/TestFeatureSystem.cpp | 34 ++++++++ .../feature/MeasurePlugin/MeasureHandler.cpp | 49 +++++------- .../feature/MeasurePlugin/MeasureHandler.h | 11 +-- .../MeasurePlugin/test/TestMeasureHandler.cpp | 38 ++++++++- 17 files changed, 233 insertions(+), 50 deletions(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index abd9634..2bb480b 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -18,10 +18,28 @@ Item{ property var resultText: "" readonly property var activeOp: App.activeOperation + readonly property bool hasInteractive: !!(activeOp && activeOp.info && activeOp.info.interactive) + readonly property bool inInteraction: hasInteractive && interactionMode === 0 + property int interactionMode: 0 + property string _interactionName: "" onActiveOpChanged: { parameters = [] resultText = "" + applyInteractionMode() + } + onInteractionModeChanged: applyInteractionMode() + onHasInteractiveChanged: applyInteractionMode() + + function applyInteractionMode() { + var want = (hasInteractive && interactionMode === 0 && activeOp) ? activeOp.info.name : "" + if (want === _interactionName) + return + if (_interactionName) + QModelManager.featureSystem.setInteractionActive(_interactionName, false) + _interactionName = want + if (want) + QModelManager.featureSystem.setInteractionActive(want, true) } // 写入参数值;功能的参数为持久参数,修改即时写回功能系统实时生效 @@ -40,24 +58,60 @@ Item{ } } - Button{ - id: commitButton - text: "执行" - enabled: !!(root.activeOp && root.activeOp.info) + Row{ + id: interactionModeSwitch + visible: root.hasInteractive + height: visible ? 34 : 0 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - height:30 - onClicked:{ - if (root.activeOp && root.activeOp.execute) - root.resultText = root.activeOp.execute(App.selection.activeComponentId, root.parameters) - if (App.registry.renderWindow) - App.registry.renderWindow.clearSelection() + spacing: 8 + RadioButton{ + text: "交互模式" + autoExclusive: false + checked: root.interactionMode === 0 + onClicked: root.interactionMode = 0 + } + RadioButton{ + text: (root.activeOp && root.activeOp.info && root.activeOp.info.execute_text) ? root.activeOp.info.execute_text : "参数执行" + autoExclusive: false + checked: root.interactionMode === 1 + onClicked: root.interactionMode = 1 + } + } + RowLayout{ + id: buttonRow + anchors.top: interactionModeSwitch.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 30 + Button{ + id: commitButton + text: "执行" + visible: !root.inInteraction + enabled: !!(root.activeOp && root.activeOp.info) + Layout.fillWidth: true + onClicked:{ + if (root.activeOp && root.activeOp.execute) + root.resultText = root.activeOp.execute(App.selection.activeComponentId, root.parameters) + if (App.registry.renderWindow) + App.registry.renderWindow.clearSelection() + } + } + Button{ + id: clearButton + text: "清除" + visible: root.inInteraction + Layout.fillWidth: true + onClicked:{ + if (App.registry.renderWindow) + App.registry.renderWindow.clearInteraction() + } } } TextArea { id: resultArea - anchors.top: commitButton.bottom + anchors.top: buttonRow.bottom anchors.left: parent.left anchors.right: parent.right // 不可见时不占锚定布局高度,避免留下空白 @@ -65,7 +119,7 @@ Item{ readOnly: true text: root.resultText wrapMode: TextEdit.Wrap - visible: text.length > 0 + visible: text.length > 0 && !root.inInteraction background: Rectangle { color: "#f0f0f0" border.color: "#d0d0d0" @@ -78,6 +132,7 @@ Item{ anchors.right: parent.right anchors.bottom: parent.bottom clip: true + visible: !root.inInteraction ColumnLayout{ anchors.fill: parent ListView{ diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.cpp b/app/model/systems/feature/QFeatureSystemAdaptor.cpp index b6e8e99..4887486 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.cpp +++ b/app/model/systems/feature/QFeatureSystemAdaptor.cpp @@ -103,6 +103,11 @@ bool QFeatureSystemAdaptor::postKeyEvent(int key, int modifiers, bool pressed) return feature_system_->dispatchKeyEvent(KeyEvent { key, modifiers, pressed }); } +bool QFeatureSystemAdaptor::setInteractionActive(const QString& unique_name, bool on) +{ + return feature_system_->setInteractionActive(unique_name.toStdString(), on); +} + void QFeatureSystemAdaptor::setActiveModel(int id) { active_model_id_ = id; diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.h b/app/model/systems/feature/QFeatureSystemAdaptor.h index 15cff6e..7a50d8e 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.h +++ b/app/model/systems/feature/QFeatureSystemAdaptor.h @@ -41,6 +41,12 @@ public: * @return 事件已被功能消费(应 accept)时为 true */ Q_INVOKABLE bool postKeyEvent(int key, int modifiers, bool pressed); + /** + * @brief 设置声明视口交互能力功能的交互激活态(侧栏交互模式切换驱动,幂等) + * @param unique_name 功能唯一名称 + * @param on 是否进入交互模式 + */ + Q_INVOKABLE bool setInteractionActive(const QString& unique_name, bool on); /** * @brief QML侧同步当前活动模型id,供功能上下文动态获取 */ diff --git a/app/render/InteractionService.cpp b/app/render/InteractionService.cpp index 8ca9305..3c5bab4 100644 --- a/app/render/InteractionService.cpp +++ b/app/render/InteractionService.cpp @@ -197,6 +197,15 @@ void InteractionService::hover(double posx, double posy) } } +void InteractionService::clear() +{ + if (!current_) + return; + if (current_->on_clear) + current_->on_clear(); + refreshAnnotations(); +} + void InteractionService::clearActors() { points_poly_->Initialize(); diff --git a/app/render/InteractionService.h b/app/render/InteractionService.h index 946d3ee..23124eb 100644 --- a/app/render/InteractionService.h +++ b/app/render/InteractionService.h @@ -48,6 +48,8 @@ public: void pick(double posx, double posy); //! @brief 悬停:同步激活状态,调用 on_hover 回调做动态预览 void hover(double posx, double posy); + //! @brief 面板"清除":调用当前状态的 on_clear 回调并刷新标注 + void clear(); private: //! @brief 同步激活状态:迁移时执行下线(on_deactivate/清标注/还原吸附)与上线(吸附/on_activate/刷新) systems::interaction::InteractionState* syncState(); diff --git a/app/render/QRenderWindow.cpp b/app/render/QRenderWindow.cpp index d1ca990..87b882b 100644 --- a/app/render/QRenderWindow.cpp +++ b/app/render/QRenderWindow.cpp @@ -555,6 +555,13 @@ void QRenderWindow::setComponentEdgeRender(Index component_id, bool is_render) }); } +void QRenderWindow::clearInteraction() +{ + dispatch_async([this](vtkRenderWindow* renderWindow, vtkUserData userData) -> void { + interaction_service_->clear(); + }); +} + void QRenderWindow::setClick() { dispatch_async([this](vtkRenderWindow* renderWindow, vtkUserData userData) { diff --git a/app/render/QRenderWindow.h b/app/render/QRenderWindow.h index 1d2a931..c1eef6f 100644 --- a/app/render/QRenderWindow.h +++ b/app/render/QRenderWindow.h @@ -111,6 +111,11 @@ public: */ Q_INVOKABLE void setFeatureAdaptor(QObject* adaptor); + /** + * @brief 清空当前交互状态与视图内标注(面板"清除"按钮) + */ + Q_INVOKABLE void clearInteraction(); + /** * @brief 显示/隐藏比例尺(测量插件激活时启用,随相机缩放自动更新刻度) * @param on 是否显示 diff --git a/app/render/test/TestInteractionService.cpp b/app/render/test/TestInteractionService.cpp index 3f86fc9..1c582b8 100644 --- a/app/render/test/TestInteractionService.cpp +++ b/app/render/test/TestInteractionService.cpp @@ -49,6 +49,7 @@ struct FakeInteraction { int activate_count = 0; int deactivate_count = 0; + int clear_count = 0; int hover_count = 0; std::vector picks; @@ -56,6 +57,7 @@ struct FakeInteraction { { state.on_activate = [this] { ++activate_count; }; state.on_deactivate = [this] { ++deactivate_count; }; + state.on_clear = [this] { ++clear_count; }; state.on_pick = [this](const PickInfo& pick) { picks.push_back(pick); return true; @@ -154,7 +156,6 @@ int main(int argc, char* argv[]) MeshActorManager mesh_manager(pts.GetPointer()); mesh_manager.bindRender(renderer.GetPointer()); - mesh_manager.loadMesh(0, test_mesh_data, renderer.GetPointer()); GeometryActorManager geometry_manager; geometry_manager.bindRender(renderer.GetPointer()); @@ -165,6 +166,10 @@ int main(int argc, char* argv[]) FakeInteraction fake; InteractionService service(*renderer, *overlay_renderer, mesh_manager.op(), sel_mgr); + // 拾取列表在服务构造时登记观察,网格须在此之后加载才会进入拾取列表 + // (与应用一致:服务于 initializeVTK 创建,模型其后加载) + mesh_manager.loadMesh(0, test_mesh_data, renderer.GetPointer()); + // 交互状态经 provider 提供:开关开启前无激活状态,pick 不转发 bool interaction_enabled = false; service.state_provider = [&]() -> systems::interaction::InteractionState* { @@ -207,6 +212,10 @@ int main(int argc, char* argv[]) service.pick(5000, 5000); // 窗口外坐标,拾取器必然未命中 check(fake.picks.size() == picks_before, "未命中吸附点时不调用 onPick"); + // ---- clear:面板"清除"转发到处理器 ---- + service.clear(); + check(fake.clear_count == 1, "clear() 转发到处理器"); + // ---- 几何(OCC)顶点拾取:geom_id 填充 ---- { TopoDS_Shape box = BRepPrimAPI_MakeBox(gp_Pnt(500.0, 500.0, 0.0), 1000.0, 800.0, 600.0).Shape(); diff --git a/model/systems/core/InteractionState.h b/model/systems/core/InteractionState.h index 2a1e0f4..b8dfce9 100644 --- a/model/systems/core/InteractionState.h +++ b/model/systems/core/InteractionState.h @@ -23,6 +23,7 @@ struct InteractionState { bool active = false; //> 交互激活态:功能经 setActive 写入(GUI 线程),渲染层读取路由拾取 std::function on_activate; //> 交互会话开始(通常清空功能内部状态) std::function on_deactivate; //> 交互会话结束 + std::function on_clear; //> 面板"清除"按钮:清空当前会话状态 //! @brief 左键拾取:返回是否有状态变化(需要刷新标注) std::function on_pick; //! @brief 悬停:返回是否更新了预览(需要刷新标注) diff --git a/model/systems/feature/FeatureSystem.cpp b/model/systems/feature/FeatureSystem.cpp index 78f999d..da0a5e2 100644 --- a/model/systems/feature/FeatureSystem.cpp +++ b/model/systems/feature/FeatureSystem.cpp @@ -163,6 +163,17 @@ interaction::InteractionState* FeatureSystem::activeInteraction() return nullptr; } +bool FeatureSystem::setInteractionActive(const std::string& unique_name, bool on) +{ + auto it = entries_.find(unique_name); + if (it == entries_.end() || !it->second.info->interactive) { + spdlog::warn("FeatureSystem::setInteractionActive: feature '{}' not found or not interactive", unique_name); + return false; + } + it->second.interaction_context.setActive(on); + return true; +} + void FeatureSystem::setOnFeatureInfosChanged(std::function callback) { on_feature_infos_changed_ = std::move(callback); diff --git a/model/systems/feature/FeatureSystem.h b/model/systems/feature/FeatureSystem.h index be3266a..8550264 100644 --- a/model/systems/feature/FeatureSystem.h +++ b/model/systems/feature/FeatureSystem.h @@ -95,6 +95,14 @@ public: * @return 声明 interactive 且 active 的功能状态;无激活交互时为 nullptr */ interaction::InteractionState* activeInteraction(); + /** + * @brief 设置指定功能的交互激活态(声明 interactive 的功能专用,UI 模式切换驱动) + * + * 单激活约定:激活一个功能会经 InteractionContext 下线其他功能的交互; + * 启停为幂等的状态应用,重复设置无副作用。 + * @return 功能存在且声明 interactive 时为 true + */ + bool setInteractionActive(const std::string& unique_name, bool on); /** * @brief 设置功能信息变更回调函数 */ diff --git a/model/systems/feature/InteractionContext.cpp b/model/systems/feature/InteractionContext.cpp index 6a6782d..df5993c 100644 --- a/model/systems/feature/InteractionContext.cpp +++ b/model/systems/feature/InteractionContext.cpp @@ -21,6 +21,11 @@ void InteractionContext::onDeactivate(std::function cb) state_->on_deactivate = std::move(cb); } +void InteractionContext::onClear(std::function cb) +{ + state_->on_clear = std::move(cb); +} + void InteractionContext::onPick(std::function cb) { state_->on_pick = std::move(cb); diff --git a/model/systems/feature/InteractionContext.h b/model/systems/feature/InteractionContext.h index 0527a21..721f637 100644 --- a/model/systems/feature/InteractionContext.h +++ b/model/systems/feature/InteractionContext.h @@ -26,6 +26,8 @@ public: void onActivate(std::function cb); //! @brief 订阅交互会话结束(渲染线程) void onDeactivate(std::function cb); + //! @brief 订阅"清除"(面板清除按钮,渲染线程) + void onClear(std::function cb); //! @brief 订阅左键拾取(渲染线程;返回是否有状态变化需要刷新标注) void onPick(std::function cb); //! @brief 订阅悬停(渲染线程;返回是否更新预览需要刷新标注) diff --git a/model/systems/feature/test/TestFeatureSystem.cpp b/model/systems/feature/test/TestFeatureSystem.cpp index 422ac5e..321a6e6 100644 --- a/model/systems/feature/test/TestFeatureSystem.cpp +++ b/model/systems/feature/test/TestFeatureSystem.cpp @@ -319,3 +319,37 @@ TEST_CASE("FeatureSystem::activeInteraction tracks interactive activation", "[Fe raw2->context->interaction.setActive(false); REQUIRE(system.activeInteraction() == nullptr); } + +TEST_CASE("FeatureSystem::setInteractionActive drives activation by feature name", "[FeatureSystem]") +{ + core::EventBus bus; + ModelLayer model_layer; + FeatureSystem system(model_layer, bus); + + auto interactive_meta = makeMetaData(); + interactive_meta.name = "InteractiveFeature"; + interactive_meta.interactive = true; + FeatureSystem::SystemHandlerPtr interactive { new FakeFeatureHandler }; + REQUIRE(system.registerHandler(interactive_meta, std::move(interactive))); + + auto plain_meta = makeMetaData(); + plain_meta.name = "PlainFeature"; + FeatureSystem::SystemHandlerPtr plain { new FakeFeatureHandler }; + REQUIRE(system.registerHandler(plain_meta, std::move(plain))); + + // 未注册功能与未声明 interactive 的功能不可激活 + REQUIRE_FALSE(system.setInteractionActive("Unknown", true)); + REQUIRE_FALSE(system.setInteractionActive("PlainFeature", true)); + REQUIRE(system.activeInteraction() == nullptr); + + // 按名激活(幂等:重复设置无副作用) + REQUIRE(system.setInteractionActive("InteractiveFeature", true)); + REQUIRE(system.setInteractionActive("InteractiveFeature", true)); + auto* active = system.activeInteraction(); + REQUIRE(active != nullptr); + REQUIRE(active->active); + + // 按名下线 + REQUIRE(system.setInteractionActive("InteractiveFeature", false)); + REQUIRE(system.activeInteraction() == nullptr); +} diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.cpp b/plugins/feature/MeasurePlugin/MeasureHandler.cpp index 0ab6d10..2a9faf6 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/MeasureHandler.cpp @@ -47,6 +47,10 @@ namespace systems::feature { namespace { using Vec3 = std::array; +// 参数下标:测量类型 / 选择对象(与 setup() 注册顺序一致) +constexpr std::size_t kTypeParam = 0; +constexpr std::size_t kSelectionParam = 1; + Vec3 operator-(const Vec3& a, const Vec3& b) { return { a[0] - b[0], a[1] - b[1], a[2] - b[2] }; @@ -911,13 +915,13 @@ std::any MeasureHandler::execute(FeatureContext& ctx) { assertExecuteThread(); - const int* type_index = ctx.params.value(0).get(); + const int* type_index = ctx.params.value(kTypeParam).get(); if (!type_index) { spdlog::error("MeasureHandler::execute: 测量类型参数无效"); return std::string("错误:测量类型参数无效"); } - const auto selection_ptr = ctx.params.value(1).get(); + const auto selection_ptr = ctx.params.value(kSelectionParam).get(); if (!selection_ptr || !*selection_ptr) { spdlog::error("MeasureHandler::execute: 选择对象参数无效"); return std::string("错误:选择对象参数无效"); @@ -964,8 +968,11 @@ std::any MeasureHandler::execute(FeatureContext& ctx) void MeasureHandler::activate(FeatureContext& ctx) { assertExecuteThread(); + // 标注集绑定到交互状态:渲染层事件后从 InteractionState.annotations 拉取绘制 + annotations_ = &ctx.interaction.annotations(); ctx.interaction.onActivate([this]() { this->clear(); }); ctx.interaction.onDeactivate([this]() { this->clear(); }); + ctx.interaction.onClear([this]() { this->clear(); }); ctx.interaction.onPick([this](const PickInfo& p) { return this->onPick(p); }); ctx.interaction.onHover([this](const PickInfo& p) { return this->onHover(p); }); } @@ -1069,21 +1076,21 @@ void MeasureHandler::addLine(const PickInfo& a, const PickInfo& b) void MeasureHandler::refreshAnnotations() { - annotations_.clear(); + annotations_->clear(); // 已确认线的端点与线段(红色端点、绿色实线) for (const MeasureLine& l : lines_) { - annotations_.points.push_back({ l.a.world_pos }); - annotations_.points.push_back({ l.b.world_pos }); - annotations_.lines.push_back({ l.a.world_pos, l.b.world_pos }); + annotations_->points.push_back({ l.a.world_pos }); + annotations_->points.push_back({ l.b.world_pos }); + annotations_->lines.push_back({ l.a.world_pos, l.b.world_pos }); } if (pending_) - annotations_.points.push_back({ pending_->world_pos }); + annotations_->points.push_back({ pending_->world_pos }); // 长度文本:每线一个,放线段中点(白色) for (const MeasureLine& l : lines_) { const Vec3 mid = midpoint(l.a.world_pos, l.b.world_pos); - annotations_.texts.push_back({ mid, "L: " + toString(length(l.b.world_pos - l.a.world_pos), 2), 1.0, 1.0, 1.0 }); + annotations_->texts.push_back({ mid, "L: " + toString(length(l.b.world_pos - l.a.world_pos), 2), 1.0, 1.0, 1.0 }); } // 夹角文本:放共点沿角平分线偏移(青色);同一点多个夹角按序号加大偏移防重叠 @@ -1114,7 +1121,7 @@ void MeasureHandler::refreshAnnotations() } } const double dist = 0.25 * std::min(lu, lv) * (1.0 + 0.3 * stack); - annotations_.texts.push_back({ { ang.at[0] + dir[0] * dist, ang.at[1] + dir[1] * dist, + annotations_->texts.push_back({ { ang.at[0] + dir[0] * dist, ang.at[1] + dir[1] * dist, ang.at[2] + dir[2] * dist }, "Ang: " + toString(ang.angle, 2), 0.3, 0.9, 1.0 }); } @@ -1128,34 +1135,14 @@ void MeasureHandler::refreshAnnotations() preview.g = 0.9; preview.b = 0.1; preview.dashed = true; - annotations_.lines.push_back(preview); + annotations_->lines.push_back(preview); const Vec3 mid = midpoint(pending_->world_pos, preview_.world_pos); - annotations_.texts.push_back({ mid, + annotations_->texts.push_back({ mid, "L: " + toString(length(preview_.world_pos - pending_->world_pos), 2), 1.0, 0.9, 0.1 }); } } -std::string MeasureHandler::resultText() const -{ - assertInteractiveThread(); - if (lines_.empty() && !pending_) - return {}; - - std::string out = "已完成直线: " + std::to_string(lines_.size()) + " 条"; - if (pending_) - out += "(已选第 1 点,再点第 2 点成线)"; - for (size_t i = 0; i < lines_.size(); ++i) { - out += "\nL" + std::to_string(i + 1) + ": " - + toString(length(lines_[i].b.world_pos - lines_[i].a.world_pos), 6); - } - for (const MeasureAngle& ang : angles_) { - out += "\n夹角(L" + std::to_string(ang.line1 + 1) + ",L" + std::to_string(ang.line2 + 1) - + "): " + toString(ang.angle, 6); - } - return out; -} - void MeasureHandler::clear() { assertInteractiveThread(); diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.h b/plugins/feature/MeasurePlugin/MeasureHandler.h index 04d115f..b3265ff 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.h +++ b/plugins/feature/MeasurePlugin/MeasureHandler.h @@ -28,7 +28,7 @@ using systems::interaction::PickInfo; * - onPick/onHover/annotations/clear 由 **渲染线程**调用 * (InteractionService 经 dispatch_async 驱动;本类在 activate() 中经 ctx.interaction 注册回调)。 * - 因此约定:**setup()/execute() 不得读写交互状态成员** - * (pending_、has_preview_、preview_、lines_、angles_、annotations_); + * (pending_、has_preview_、preview_、lines_、angles_); * **交互事件方法不得触碰标注执行路径的状态**(execute 当前是无状态的,新增状态前必须评审)。 * - 本类**无锁**。UI 层的模式互斥(交互模式隐藏执行按钮、参数模式停止交互)只是当前的安全网, * 不是设计依据;确需跨两侧共享状态时,先加锁或经设计评审,不得直接访问。 @@ -41,7 +41,7 @@ public: //! @brief 声明功能参数(测量类型、选择对象)与菜单项 void setup(FeatureRegistrar& reg) override; - //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用) + //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用/清除) void activate(FeatureContext& ctx) override; //! @brief 尺寸标注:按参数中的测量类型与选择对象执行,返回结果文本 std::any execute(FeatureContext& ctx) override; @@ -54,8 +54,7 @@ private: // ---- 交互回调(原 InteractiveHandler 接口方法,现由 activate() 注册到 InteractionContext) ---- bool onPick(const PickInfo& pick); bool onHover(const PickInfo& pick); - const AnnotationBatch& annotations() const { assertInteractiveThread(); return annotations_; } - std::string resultText() const; + const AnnotationBatch& annotations() const { assertInteractiveThread(); return *annotations_; } void clear(); //! @brief 交互测量线:两个吸附点(PickInfo 已含世界坐标与两套顶点 id) @@ -89,7 +88,9 @@ private: // 以下交互状态成员仅允许渲染线程的交互方法访问(见类注释的线程模型约定) std::vector lines_; std::vector angles_; - AnnotationBatch annotations_; + // 标注集契约:功能在回调中直接更新 InteractionState.annotations(activate 时绑定), + // 渲染层事件后拉取绘制;自持成员会导致渲染层永远拉到空标注 + AnnotationBatch* annotations_ { nullptr }; const std::thread::id gui_thread_id_ = std::this_thread::get_id(); //> 构造线程(GUI 线程)id mutable std::thread::id interactive_thread_id_ {}; //> 首个交互调用所在线程 id diff --git a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp index d9f80b0..99da86e 100644 --- a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp @@ -77,7 +77,7 @@ struct FeatureTestEnv { }); } - //! @brief 写入功能参数并执行尺寸标注,返回结果字符串 + //! @brief 写入功能参数并执行尺寸标注,返回结果字符串(下标 0=测量类型 1=选择对象,见 setup) std::string executeMeasure(int measure_type_index, std::shared_ptr selection) { params->setValue(0, core::ArgObject::create(measure_type_index)); @@ -153,6 +153,42 @@ TEST_CASE("MeasureHandler setup declares measure parameters and menu") REQUIRE(reg.menuItems().size() == 1); } +TEST_CASE("MeasureHandler: interactive picks update state annotations and onClear resets") +{ + FeatureTestEnv env; + env.handler.activate(*env.ctx); + REQUIRE(env.interaction_state_.on_pick); + + // 两点成线:(0,0,0) → (1,0,0) + systems::interaction::PickInfo p1; + p1.valid = true; + p1.world_pos = { 0.0, 0.0, 0.0 }; + p1.mesh_id = 0; + systems::interaction::PickInfo p2; + p2.valid = true; + p2.world_pos = { 1.0, 0.0, 0.0 }; + p2.mesh_id = 1; + + env.interaction_state_.on_pick(p1); + CHECK(env.handler.hasPending()); + env.interaction_state_.on_pick(p2); + CHECK(env.handler.lineCount() == 1); + + // 交互标注写入 InteractionState.annotations(渲染层拉取绘制的契约) + CHECK(env.interaction_state_.annotations.lines.size() == 1); + CHECK(env.interaction_state_.annotations.points.size() == 2); + CHECK(env.interaction_state_.annotations.texts.size() == 1); + + // 面板"清除":on_clear 清空会话状态与标注 + REQUIRE(env.interaction_state_.on_clear); + env.interaction_state_.on_clear(); + CHECK(env.handler.lineCount() == 0); + CHECK(!env.handler.hasPending()); + CHECK(env.interaction_state_.annotations.lines.empty()); + CHECK(env.interaction_state_.annotations.points.empty()); + CHECK(env.interaction_state_.annotations.texts.empty()); +} + TEST_CASE("MeasureHandler: distance between two vertices") { auto mesh = std::make_unique(MakeMeshData()); -- Gitee From f3c9e03c84ad693d5ef6a96c435480cacd8271b3 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sat, 25 Jul 2026 22:40:45 +0800 Subject: [PATCH 03/13] =?UTF-8?q?refactor(systems,app):=20=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E6=BF=80=E6=B4=BB=E6=94=B9=E4=B8=BA=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E9=A9=B1=E5=8A=A8=E5=B9=B6=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=A3=B0=E6=98=8E=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FeatureSystem::setInteractionActive 改为单参数:传功能名激活(单激活约定自动下线其他功能),空串表示全部下线(活动操作无交互能力) - 幂等守卫下沉 InteractionContext::setActive:目标状态已达即返回,重复启停无副作用,QML 不再持有激活状态变量 - 删除 execute_text / interaction_guide 声明链:交互与参数执行拆分为独立功能后,模式命名与交互说明失去存在必要,FeatureInfo / HandlerMetaData / json 解析 / QFeatureInfo / adaptor 全链回退 - SideBar 删除交互/参数双模式切换(单选按钮与模式状态机),按钮区按 interactive 声明位二选一:交互功能显示"清除",其余显示"执行" - TestFeatureSystem 同步:setInteractionActive 单参数、空串下线、幂等用例 --- app/SideBar.qml | 49 +++---------------- app/model/systems/feature/QFeatureInfo.h | 10 +--- .../systems/feature/QFeatureSystemAdaptor.cpp | 8 ++- .../systems/feature/QFeatureSystemAdaptor.h | 7 ++- model/systems/feature/FeatureInfo.h | 2 - model/systems/feature/FeatureSystem.cpp | 13 +++-- model/systems/feature/FeatureSystem.h | 11 ++--- .../systems/feature/FeatureSystemRegister.cpp | 2 - model/systems/feature/InteractionContext.cpp | 3 ++ .../feature/test/TestFeatureSystem.cpp | 22 +++------ 10 files changed, 38 insertions(+), 89 deletions(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index 2bb480b..3a3fd3e 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -19,27 +19,12 @@ Item{ readonly property var activeOp: App.activeOperation readonly property bool hasInteractive: !!(activeOp && activeOp.info && activeOp.info.interactive) - readonly property bool inInteraction: hasInteractive && interactionMode === 0 - property int interactionMode: 0 - property string _interactionName: "" onActiveOpChanged: { parameters = [] resultText = "" - applyInteractionMode() - } - onInteractionModeChanged: applyInteractionMode() - onHasInteractiveChanged: applyInteractionMode() - - function applyInteractionMode() { - var want = (hasInteractive && interactionMode === 0 && activeOp) ? activeOp.info.name : "" - if (want === _interactionName) - return - if (_interactionName) - QModelManager.featureSystem.setInteractionActive(_interactionName, false) - _interactionName = want - if (want) - QModelManager.featureSystem.setInteractionActive(want, true) + // 活动操作声明视口交互能力则激活其交互,否则全部下线(幂等,守卫在功能系统内) + QModelManager.featureSystem.setInteractionActive(hasInteractive ? activeOp.info.name : "") } // 写入参数值;功能的参数为持久参数,修改即时写回功能系统实时生效 @@ -58,37 +43,16 @@ Item{ } } - Row{ - id: interactionModeSwitch - visible: root.hasInteractive - height: visible ? 34 : 0 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - spacing: 8 - RadioButton{ - text: "交互模式" - autoExclusive: false - checked: root.interactionMode === 0 - onClicked: root.interactionMode = 0 - } - RadioButton{ - text: (root.activeOp && root.activeOp.info && root.activeOp.info.execute_text) ? root.activeOp.info.execute_text : "参数执行" - autoExclusive: false - checked: root.interactionMode === 1 - onClicked: root.interactionMode = 1 - } - } RowLayout{ id: buttonRow - anchors.top: interactionModeSwitch.bottom + anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: 30 Button{ id: commitButton text: "执行" - visible: !root.inInteraction + visible: !root.hasInteractive enabled: !!(root.activeOp && root.activeOp.info) Layout.fillWidth: true onClicked:{ @@ -101,7 +65,7 @@ Item{ Button{ id: clearButton text: "清除" - visible: root.inInteraction + visible: root.hasInteractive Layout.fillWidth: true onClicked:{ if (App.registry.renderWindow) @@ -119,7 +83,7 @@ Item{ readOnly: true text: root.resultText wrapMode: TextEdit.Wrap - visible: text.length > 0 && !root.inInteraction + visible: text.length > 0 background: Rectangle { color: "#f0f0f0" border.color: "#d0d0d0" @@ -132,7 +96,6 @@ Item{ anchors.right: parent.right anchors.bottom: parent.bottom clip: true - visible: !root.inInteraction ColumnLayout{ anchors.fill: parent ListView{ diff --git a/app/model/systems/feature/QFeatureInfo.h b/app/model/systems/feature/QFeatureInfo.h index f33e615..2a6932b 100644 --- a/app/model/systems/feature/QFeatureInfo.h +++ b/app/model/systems/feature/QFeatureInfo.h @@ -17,11 +17,9 @@ class QFeatureInfo : public QObject { Q_PROPERTY(QString icon READ icon CONSTANT) Q_PROPERTY(QList arg_types READ argTypes CONSTANT) Q_PROPERTY(bool interactive READ interactive CONSTANT) - Q_PROPERTY(QString execute_text READ executeText CONSTANT) - Q_PROPERTY(QString interaction_guide READ interactionGuide CONSTANT) public: QFeatureInfo(QString name, QString display_name, QString description, QString menu_path, QString icon, QList arg_types, - bool interactive = false, QString execute_text = {}, QString interaction_guide = {}, QObject* parent = nullptr) + bool interactive = false, QObject* parent = nullptr) : QObject(parent) , name_(std::move(name)) , display_name_(std::move(display_name)) @@ -30,8 +28,6 @@ public: , icon_(std::move(icon)) , arg_types_(std::move(arg_types)) , interactive_(interactive) - , execute_text_(std::move(execute_text)) - , interaction_guide_(std::move(interaction_guide)) { } QString name() const { return name_; } @@ -41,8 +37,6 @@ public: QString icon() const { return icon_; } QList argTypes() const { return arg_types_; } bool interactive() const { return interactive_; } - QString executeText() const { return execute_text_; } - QString interactionGuide() const { return interaction_guide_; } private: QString name_; //> 功能唯一名称,用作索引 @@ -52,7 +46,5 @@ private: QString icon_; //> 自定义图标的 qrc 资源路径,为空时按插件名映射默认图标 QList arg_types_; //> 功能参数类型列表 bool interactive_ = false; //> 是否声明视口交互能力 - QString execute_text_; //> 参数执行模式的 UI 名称(interactive 功能专用) - QString interaction_guide_; //> 交互模式的操作说明文字(interactive 功能专用) }; #endif // !Q_FEATURE_INFO_H diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.cpp b/app/model/systems/feature/QFeatureSystemAdaptor.cpp index 4887486..36d2846 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.cpp +++ b/app/model/systems/feature/QFeatureSystemAdaptor.cpp @@ -103,9 +103,9 @@ bool QFeatureSystemAdaptor::postKeyEvent(int key, int modifiers, bool pressed) return feature_system_->dispatchKeyEvent(KeyEvent { key, modifiers, pressed }); } -bool QFeatureSystemAdaptor::setInteractionActive(const QString& unique_name, bool on) +bool QFeatureSystemAdaptor::setInteractionActive(const QString& unique_name) { - return feature_system_->setInteractionActive(unique_name.toStdString(), on); + return feature_system_->setInteractionActive(unique_name.toStdString()); } void QFeatureSystemAdaptor::setActiveModel(int id) @@ -139,9 +139,7 @@ QList QFeatureSystemAdaptor::getFeaturesInfo() const QString::fromStdString(menu.menu_path.empty() ? "功能" : menu.menu_path), QString::fromStdString(menu.icon), std::move(args), - feature_info->interactive, - QString::fromStdString(feature_info->execute_text), - QString::fromStdString(feature_info->interaction_guide))); + feature_info->interactive)); } } return infos; diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.h b/app/model/systems/feature/QFeatureSystemAdaptor.h index 7a50d8e..5391a2d 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.h +++ b/app/model/systems/feature/QFeatureSystemAdaptor.h @@ -42,11 +42,10 @@ public: */ Q_INVOKABLE bool postKeyEvent(int key, int modifiers, bool pressed); /** - * @brief 设置声明视口交互能力功能的交互激活态(侧栏交互模式切换驱动,幂等) - * @param unique_name 功能唯一名称 - * @param on 是否进入交互模式 + * @brief 设置当前活动功能的交互激活态(活动操作切换驱动,幂等) + * @param unique_name 要激活的功能唯一名称(须声明 interactive);空串表示全部下线 */ - Q_INVOKABLE bool setInteractionActive(const QString& unique_name, bool on); + Q_INVOKABLE bool setInteractionActive(const QString& unique_name); /** * @brief QML侧同步当前活动模型id,供功能上下文动态获取 */ diff --git a/model/systems/feature/FeatureInfo.h b/model/systems/feature/FeatureInfo.h index da80beb..3167484 100644 --- a/model/systems/feature/FeatureInfo.h +++ b/model/systems/feature/FeatureInfo.h @@ -35,8 +35,6 @@ struct FeatureInfo { std::string display_name; //> 功能 UI 展示用名称 std::string description; //> 功能描述 bool interactive = false; //> 是否声明视口交互能力(功能经 interaction 上下文订阅交互回调) - std::string execute_text; //> 参数执行模式的 UI 名称(interactive 功能专用,空时 UI 用默认"参数执行") - std::string interaction_guide; //> 交互模式的操作说明文字(interactive 功能专用) std::vector arg_types; //> 功能参数类型列表 std::vector menus; //> 菜单贡献项列表 std::vector key_bindings; //> 按键绑定列表 diff --git a/model/systems/feature/FeatureSystem.cpp b/model/systems/feature/FeatureSystem.cpp index da0a5e2..0890433 100644 --- a/model/systems/feature/FeatureSystem.cpp +++ b/model/systems/feature/FeatureSystem.cpp @@ -48,8 +48,6 @@ bool FeatureSystem::registerHandler(const HandlerMetaData& meta_data, SystemHand info->display_name = meta_data.display_name; info->description = meta_data.description; info->interactive = meta_data.interactive; - info->execute_text = meta_data.execute_text; - info->interaction_guide = meta_data.interaction_guide; info->arg_types = registrar.argTypes(); info->menus = registrar.menuItems(); info->key_bindings = registrar.keyBindings(); @@ -163,14 +161,21 @@ interaction::InteractionState* FeatureSystem::activeInteraction() return nullptr; } -bool FeatureSystem::setInteractionActive(const std::string& unique_name, bool on) +bool FeatureSystem::setInteractionActive(const std::string& unique_name) { + // 空串 = 活动操作无交互能力:全部下线(重复调用由 InteractionContext 的目标状态守卫兜底) + if (unique_name.empty()) { + for (auto&& [name, entry] : entries_) + entry.interaction_context.setActive(false); + return true; + } + auto it = entries_.find(unique_name); if (it == entries_.end() || !it->second.info->interactive) { spdlog::warn("FeatureSystem::setInteractionActive: feature '{}' not found or not interactive", unique_name); return false; } - it->second.interaction_context.setActive(on); + it->second.interaction_context.setActive(true); // 单激活约定:自动下线其他功能 return true; } diff --git a/model/systems/feature/FeatureSystem.h b/model/systems/feature/FeatureSystem.h index 8550264..41e4c07 100644 --- a/model/systems/feature/FeatureSystem.h +++ b/model/systems/feature/FeatureSystem.h @@ -37,8 +37,6 @@ struct HandlerMetaData { std::string display_name {}; //> 功能 UI 展示用名称 std::string description {}; //> 功能描述 bool interactive {}; //> 是否声明视口交互能力(功能经 interaction 上下文订阅交互回调) - std::string execute_text {}; //> 参数执行模式的 UI 名称(interactive 功能专用,空时 UI 用默认"参数执行") - std::string interaction_guide {}; //> 交互模式的操作说明文字(interactive 功能专用) }; /** @@ -96,13 +94,14 @@ public: */ interaction::InteractionState* activeInteraction(); /** - * @brief 设置指定功能的交互激活态(声明 interactive 的功能专用,UI 模式切换驱动) + * @brief 设置当前活动功能的交互激活态(声明 interactive 的功能专用,活动操作切换驱动) * * 单激活约定:激活一个功能会经 InteractionContext 下线其他功能的交互; - * 启停为幂等的状态应用,重复设置无副作用。 - * @return 功能存在且声明 interactive 时为 true + * 启停为幂等的状态应用(InteractionContext 以目标状态为守卫),重复设置无副作用。 + * @param unique_name 要激活的功能唯一名称;空串表示全部下线(活动操作无交互能力) + * @return 名称为空,或功能存在且声明 interactive 时为 true */ - bool setInteractionActive(const std::string& unique_name, bool on); + bool setInteractionActive(const std::string& unique_name); /** * @brief 设置功能信息变更回调函数 */ diff --git a/model/systems/feature/FeatureSystemRegister.cpp b/model/systems/feature/FeatureSystemRegister.cpp index 6e2a163..cf8a642 100644 --- a/model/systems/feature/FeatureSystemRegister.cpp +++ b/model/systems/feature/FeatureSystemRegister.cpp @@ -40,8 +40,6 @@ HandlerMetaData FeatureSystemRegister::toMetaData(const QJsonObject& meta_data) handler_data.display_name = meta_data.value("display_name").toString().toStdString(); handler_data.description = meta_data.value("description").toString().toStdString(); handler_data.interactive = meta_data.value("interactive").toBool(false); - handler_data.execute_text = meta_data.value("execute_text").toString().toStdString(); - handler_data.interaction_guide = meta_data.value("interaction_guide").toString().toStdString(); return handler_data; } } diff --git a/model/systems/feature/InteractionContext.cpp b/model/systems/feature/InteractionContext.cpp index df5993c..39c4fd3 100644 --- a/model/systems/feature/InteractionContext.cpp +++ b/model/systems/feature/InteractionContext.cpp @@ -43,6 +43,9 @@ systems::interaction::AnnotationBatch& InteractionContext::annotations() void InteractionContext::setActive(bool on) { + // 幂等守卫:目标状态已达则直接返回,重复启停无副作用 + if (state_->active == on) + return; // 单激活约定:激活自己前先下线其他功能的交互 if (on && deactivate_others_) { deactivate_others_(); diff --git a/model/systems/feature/test/TestFeatureSystem.cpp b/model/systems/feature/test/TestFeatureSystem.cpp index 321a6e6..015244d 100644 --- a/model/systems/feature/test/TestFeatureSystem.cpp +++ b/model/systems/feature/test/TestFeatureSystem.cpp @@ -244,7 +244,7 @@ TEST_CASE("FeatureSystem re-registering same name replaces old handler", "[Featu REQUIRE(system.getFeatureInfos().size() == 1); } -TEST_CASE("FeatureSystem propagates interactive and text metadata to FeatureInfo", "[FeatureSystem]") +TEST_CASE("FeatureSystem propagates interactive metadata to FeatureInfo", "[FeatureSystem]") { core::EventBus bus; ModelLayer model_layer; @@ -252,18 +252,14 @@ TEST_CASE("FeatureSystem propagates interactive and text metadata to FeatureInfo auto meta_data = makeMetaData(); meta_data.interactive = true; - meta_data.execute_text = "尺寸标注"; - meta_data.interaction_guide = "点击两点成线"; FeatureSystem::SystemHandlerPtr handler { new FakeFeatureHandler }; REQUIRE(system.registerHandler(meta_data, std::move(handler))); auto infos = system.getFeatureInfos(); REQUIRE(infos.size() == 1); REQUIRE(infos[0]->interactive); - REQUIRE(infos[0]->execute_text == "尺寸标注"); - REQUIRE(infos[0]->interaction_guide == "点击两点成线"); - // 未声明时为空串与 false + // 未声明时为 false FeatureSystem::SystemHandlerPtr plain { new FakeFeatureHandler }; auto plain_meta = makeMetaData(); plain_meta.name = "PlainFeature"; @@ -273,8 +269,6 @@ TEST_CASE("FeatureSystem propagates interactive and text metadata to FeatureInfo for (const FeatureInfo* info : infos) { if (info->name == "PlainFeature") { REQUIRE_FALSE(info->interactive); - REQUIRE(info->execute_text.empty()); - REQUIRE(info->interaction_guide.empty()); } } } @@ -338,18 +332,18 @@ TEST_CASE("FeatureSystem::setInteractionActive drives activation by feature name REQUIRE(system.registerHandler(plain_meta, std::move(plain))); // 未注册功能与未声明 interactive 的功能不可激活 - REQUIRE_FALSE(system.setInteractionActive("Unknown", true)); - REQUIRE_FALSE(system.setInteractionActive("PlainFeature", true)); + REQUIRE_FALSE(system.setInteractionActive("Unknown")); + REQUIRE_FALSE(system.setInteractionActive("PlainFeature")); REQUIRE(system.activeInteraction() == nullptr); // 按名激活(幂等:重复设置无副作用) - REQUIRE(system.setInteractionActive("InteractiveFeature", true)); - REQUIRE(system.setInteractionActive("InteractiveFeature", true)); + REQUIRE(system.setInteractionActive("InteractiveFeature")); + REQUIRE(system.setInteractionActive("InteractiveFeature")); auto* active = system.activeInteraction(); REQUIRE(active != nullptr); REQUIRE(active->active); - // 按名下线 - REQUIRE(system.setInteractionActive("InteractiveFeature", false)); + // 活动操作切到无交互能力的功能:空串全部下线 + REQUIRE(system.setInteractionActive("")); REQUIRE(system.activeInteraction() == nullptr); } -- Gitee From 104396fd1f6b853db5efdbc7f83d465428f0a099 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sat, 25 Jul 2026 22:40:54 +0800 Subject: [PATCH 04/13] =?UTF-8?q?refactor(plugins):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E6=B5=8B=E9=87=8F=E6=8F=92=E4=BB=B6=E4=B8=BA"=E6=B5=8B?= =?UTF-8?q?=E9=87=8F"=E4=B8=8E"=E5=B0=BA=E5=AF=B8=E6=A0=87=E6=B3=A8"?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E7=8B=AC=E7=AB=8B=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原 MeasurePlugin 以双模式承载两类功能,模式切换本质是两类功能共用一个注册位的 UI 补丁,拆分为: - MeasurePlugin(测量):纯视口交互——两点成线测距、共端点两线夹角标注、悬停预览、面板清除;无参数无执行路径。MeasureHandler 1156 行精简至 243 行:线程模型契约注释与线程亲和断言整体删除(全部状态仅渲染线程的交互回调访问),CMake 移除不再使用的 OCC 链接 - DimensionPlugin(尺寸标注):纯参数执行——距离/角度/半径/长度/面积/体积/包围盒/重心,网格与 OCC 几何双路径,继承原测量操作注册表;execute 无状态、纯 GUI 线程 - 测试拆分:TestDimensionHandler 继承全部参数执行用例(网格三点/边对与 OCC 盒体共 7 例),TestMeasureHandler 保留声明与交互回调用例(2 例),连同 FeatureSystem、InteractionService 测试全部通过 --- plugins/feature/CMakeLists.txt | 1 + .../feature/DimensionPlugin/CMakeLists.txt | 12 + .../DimensionPlugin/DimensionHandler.cpp | 962 ++++++++++++++++++ .../DimensionPlugin/DimensionHandler.h | 27 + .../feature/DimensionPlugin/DimensionPlugin.h | 24 + .../DimensionPlugin/DimensionPlugin.json | 8 + .../DimensionPlugin/test/CMakeLists.txt | 2 + .../test/TestDimensionHandler.cpp | 268 +++++ plugins/feature/MeasurePlugin/CMakeLists.txt | 6 - .../feature/MeasurePlugin/MeasureHandler.cpp | 919 +---------------- .../feature/MeasurePlugin/MeasureHandler.h | 39 +- .../feature/MeasurePlugin/MeasurePlugin.json | 6 +- .../feature/MeasurePlugin/test/CMakeLists.txt | 2 +- .../MeasurePlugin/test/TestMeasureHandler.cpp | 212 +--- 14 files changed, 1322 insertions(+), 1166 deletions(-) create mode 100644 plugins/feature/DimensionPlugin/CMakeLists.txt create mode 100644 plugins/feature/DimensionPlugin/DimensionHandler.cpp create mode 100644 plugins/feature/DimensionPlugin/DimensionHandler.h create mode 100644 plugins/feature/DimensionPlugin/DimensionPlugin.h create mode 100644 plugins/feature/DimensionPlugin/DimensionPlugin.json create mode 100644 plugins/feature/DimensionPlugin/test/CMakeLists.txt create mode 100644 plugins/feature/DimensionPlugin/test/TestDimensionHandler.cpp diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index b678832..2a45edb 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(FeatureDemoPlugin) add_subdirectory(MeasurePlugin) +add_subdirectory(DimensionPlugin) diff --git a/plugins/feature/DimensionPlugin/CMakeLists.txt b/plugins/feature/DimensionPlugin/CMakeLists.txt new file mode 100644 index 0000000..2240720 --- /dev/null +++ b/plugins/feature/DimensionPlugin/CMakeLists.txt @@ -0,0 +1,12 @@ +precess_add_feature_plugin(DimensionPlugin + SOURCES "DimensionHandler.cpp" + PLUGIN_H "DimensionPlugin.h" +) + +precess_plugin_link_libraries(DimensionPlugin + TKernel # Standard / gp 基础类型 + TKBRep # BRep_Tool、BRepAdaptor + TKTopAlgo # BRepGProp、BRepBndLib +) + +add_subdirectory(test) diff --git a/plugins/feature/DimensionPlugin/DimensionHandler.cpp b/plugins/feature/DimensionPlugin/DimensionHandler.cpp new file mode 100644 index 0000000..07c78a0 --- /dev/null +++ b/plugins/feature/DimensionPlugin/DimensionHandler.cpp @@ -0,0 +1,962 @@ +/** + * @file DimensionHandler.cpp + * @brief 尺寸标注处理器:网格与几何双路径的距离、角度、半径、长度、面积、体积、包围盒与重心 + * @author 范成通 email 1941804585@qq.com + */ + +#include "DimensionHandler.h" +#include "ArgObject.h" +#include "ComponentData.h" +#include "FeatureContext.h" +#include "FeatureParams.h" +#include "FeatureRegistrar.h" +#include "GeometryData.h" +#include "GeometryRegistry.h" +#include "MeshData.h" +#include "ModelLayer.h" +#include "Selection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace systems::feature { + +namespace { +using Vec3 = std::array; + +// 参数下标:测量类型 / 选择对象(与 setup() 注册顺序一致) +constexpr std::size_t kTypeParam = 0; +constexpr std::size_t kSelectionParam = 1; + +Vec3 operator-(const Vec3& a, const Vec3& b) +{ + return { a[0] - b[0], a[1] - b[1], a[2] - b[2] }; +} + +Vec3 operator+(const Vec3& a, const Vec3& b) +{ + return { a[0] + b[0], a[1] + b[1], a[2] + b[2] }; +} + +Vec3 operator*(const Vec3& a, double s) +{ + return { a[0] * s, a[1] * s, a[2] * s }; +} + +Vec3 operator/(const Vec3& a, double s) +{ + return { a[0] / s, a[1] / s, a[2] / s }; +} + +//! @brief 两点中点 +Vec3 midpoint(const Vec3& a, const Vec3& b) +{ + return { (a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, (a[2] + b[2]) / 2.0 }; +} + +double dot(const Vec3& a, const Vec3& b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +double length(const Vec3& v) +{ + return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); +} + +Vec3 cross(const Vec3& a, const Vec3& b) +{ + return { + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; +} + +enum class MeasureType { + Distance, + Angle, + Radius, + Length, + Area, + Volume, + BoundingBox, + Centroid +}; + +//! @brief 网格测量实现签名 +using MeshExecFn = std::string (*)(const MeshData&, const ModelLayer&, const Selection&); +//! @brief 几何测量实现签名 +using GeomExecFn = std::string (*)(const GeometryRegistry&, const Selection&); + +//! @brief 测量操作表项:名称、选择器模式与两套实现的统一注册处(新增类型只需在 kMeasureOps 加一行) +struct MeasureOp { + MeasureType type; + const char* name; //> Combo 显示名(表内顺序即下标) + const char* mesh_selector_modes; //> 网格组件的选择器模式 + const char* geom_selector_modes; //> 几何组件的选择器模式 + MeshExecFn mesh_exec; //> 网格实现 + GeomExecFn geom_exec; //> 几何实现(nullptr 表示暂不支持几何模型) +}; + +bool isValidVertexId(const MeshData& mesh, const ModelLayer& manager, Index id) +{ + if (id < 0) + return false; + if (mesh.local_to_global_.empty()) + return id < static_cast(mesh.vertex_positions_.size()); + return id < static_cast(manager.globalPoints().size()); +} + +const Vec3& getPosition(const MeshData& mesh, const ModelLayer& manager, Index id) +{ + if (!mesh.local_to_global_.empty() && !manager.globalPoints().empty()) + return manager.globalPoints()[id]; + return mesh.vertex_positions_[id]; +} + +std::string toString(double value, int precision = 6) +{ + std::ostringstream oss; + oss.setf(std::ios::fixed, std::ios::floatfield); + oss.precision(precision); + oss << value; + return oss.str(); +} + +std::string vecString(const Vec3& v) +{ + return "(" + toString(v[0]) + ", " + toString(v[1]) + ", " + toString(v[2]) + ")"; +} + +double angleBetween(const Vec3& u, const Vec3& v) +{ + const double lu = length(u); + const double lv = length(v); + if (lu < std::numeric_limits::epsilon() || lv < std::numeric_limits::epsilon()) + return 0.0; + + double cos_theta = dot(u, v) / (lu * lv); + cos_theta = std::max(-1.0, std::min(1.0, cos_theta)); + return std::acos(cos_theta) * 180.0 / 3.14159265358979323846; +} + +double triangleArea(const Vec3& a, const Vec3& b, const Vec3& c) +{ + return 0.5 * length(cross(b - a, c - a)); +} + +double polygonArea(const std::vector& points) +{ + const size_t n = points.size(); + if (n < 3) + return 0.0; + + double area = 0.0; + for (size_t i = 2; i < n; ++i) + area += triangleArea(points[0], points[i - 1], points[i]); + return area; +} + +double tetraVolume(const Vec3& a, const Vec3& b, const Vec3& c, const Vec3& d) +{ + return std::abs(dot(b - a, cross(c - a, d - a))) / 6.0; +} + +double polyhedronVolume(const MeshData& mesh, const ModelLayer& manager, Index solid_id) +{ + if (solid_id + 1 >= static_cast(mesh.solid_faces_offset_.size())) + return 0.0; + + const Index face_start = mesh.solid_faces_offset_[solid_id]; + const Index face_end = mesh.solid_faces_offset_[solid_id + 1]; + double volume = 0.0; + + for (Index f = face_start; f < face_end; ++f) { + const Index face_id = mesh.solid_faces_[f]; + if (face_id + 1 >= static_cast(mesh.solid_faces_vertices_offset_.size())) + continue; + + const Index vert_start = mesh.solid_faces_vertices_offset_[face_id]; + const Index vert_end = mesh.solid_faces_vertices_offset_[face_id + 1]; + if (vert_end - vert_start < 3) + continue; + + const Vec3& v0 = getPosition(mesh, manager, mesh.solid_faces_vertices_[vert_start]); + for (Index i = vert_start + 1; i + 1 < vert_end; ++i) { + const Vec3& vi = getPosition(mesh, manager, mesh.solid_faces_vertices_[i]); + const Vec3& vj = getPosition(mesh, manager, mesh.solid_faces_vertices_[i + 1]); + volume += dot(v0, cross(vi, vj)) / 6.0; + } + } + + return std::abs(volume); +} + +double solidVolume(const MeshData& mesh, const ModelLayer& manager, Index solid_id) +{ + if (solid_id < 0 || solid_id + 1 >= static_cast(mesh.solid_vertices_offset_.size())) + return 0.0; + if (solid_id >= static_cast(mesh.solid_types_.size())) + return 0.0; + + const unsigned char cell_type = mesh.solid_types_[solid_id]; + const Index start = mesh.solid_vertices_offset_[solid_id]; + const Index end = mesh.solid_vertices_offset_[solid_id + 1]; + + // VTK_TETRA = 10 + if (cell_type == 10) { + if (end - start != 4) + return 0.0; + const Vec3& a = getPosition(mesh, manager, mesh.solid_vertices_[start]); + const Vec3& b = getPosition(mesh, manager, mesh.solid_vertices_[start + 1]); + const Vec3& c = getPosition(mesh, manager, mesh.solid_vertices_[start + 2]); + const Vec3& d = getPosition(mesh, manager, mesh.solid_vertices_[start + 3]); + return tetraVolume(a, b, c, d); + } + + // VTK_POLYHEDRON = 42 + if (cell_type == 42) + return polyhedronVolume(mesh, manager, solid_id); + + return 0.0; +} + +std::vector collectPositions(const MeshData& mesh, const ModelLayer& manager, + ElementEnum::Type type, const std::vector& ids) +{ + std::vector positions; + positions.reserve(ids.size() * 4); + + switch (type) { + case ElementEnum::Vertex: + case ElementEnum::Edge: + // 边选择的 ids 即每条边端点的顶点 id(EdgeSelectorHighlight 约定),与点一样按顶点处理 + for (Index id : ids) { + if (isValidVertexId(mesh, manager, id)) + positions.push_back(getPosition(mesh, manager, id)); + } + break; + case ElementEnum::Face: + for (Index id : ids) { + if (id + 1 >= static_cast(mesh.face_vertices_offset_.size())) + continue; + for (Index i = mesh.face_vertices_offset_[id]; i < mesh.face_vertices_offset_[id + 1]; ++i) { + const Index v = mesh.face_vertices_[i]; + if (isValidVertexId(mesh, manager, v)) + positions.push_back(getPosition(mesh, manager, v)); + } + } + break; + case ElementEnum::Solid: + for (Index id : ids) { + if (id + 1 >= static_cast(mesh.solid_vertices_offset_.size())) + continue; + for (Index i = mesh.solid_vertices_offset_[id]; i < mesh.solid_vertices_offset_[id + 1]; ++i) { + const Index v = mesh.solid_vertices_[i]; + if (isValidVertexId(mesh, manager, v)) + positions.push_back(getPosition(mesh, manager, v)); + } + } + break; + default: + break; + } + + return positions; +} + +std::string formatDistance(const Vec3& a, const Vec3& b) +{ + const Vec3 d = b - a; + std::ostringstream oss; + oss << "Distance: " << toString(length(d)) << "\n" + << "Dx: " << toString(d[0]) << "\n" + << "Dy: " << toString(d[1]) << "\n" + << "Dz: " << toString(d[2]); + return oss.str(); +} + +std::string formatAngle(const Vec3& a, const Vec3& b, const Vec3& c) +{ + std::ostringstream oss; + oss << "Angle: " << toString(angleBetween(a - b, c - b)) << " deg"; + return oss.str(); +} + +std::string formatRadius(const Vec3& a, const Vec3& b, const Vec3& c) +{ + const Vec3 ab = b - a; + const Vec3 ac = c - a; + const Vec3 n = cross(ab, ac); + const double n_len2 = dot(n, n); + + if (n_len2 < std::numeric_limits::epsilon()) + return "错误:三点共线或重合,无法计算半径"; + + const Vec3 circumcenter = a + (cross(ac, n) * dot(ab, ab) - cross(ab, n) * dot(ac, ac)) / (2.0 * n_len2); + const double radius = length(circumcenter - a); + + std::ostringstream oss; + oss << "Radius: " << toString(radius) << "\n" + << "Center: " << vecString(circumcenter); + return oss.str(); +} + +std::string formatEdgeLength(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + // 边选择的 ids 是每条边两个端点的顶点 id 对,逐对计算长度 + for (size_t i = 0; i + 1 < ids.size(); i += 2) { + const Index v0 = ids[i]; + const Index v1 = ids[i + 1]; + const size_t edge_no = i / 2; + if (!isValidVertexId(mesh, manager, v0) || !isValidVertexId(mesh, manager, v1)) { + oss << "边 " << edge_no << ": 无效顶点\n"; + continue; + } + + const double len = length(getPosition(mesh, manager, v1) - getPosition(mesh, manager, v0)); + total += len; + oss << "边 " << edge_no << ": " << toString(len) << "\n"; + } + + oss << "累计长度: " << toString(total); + return oss.str(); +} + +std::string formatFaceArea(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + if (id + 1 >= static_cast(mesh.face_vertices_offset_.size())) { + oss << "面 " << id << ": 无效索引\n"; + continue; + } + + std::vector points; + for (Index i = mesh.face_vertices_offset_[id]; i < mesh.face_vertices_offset_[id + 1]; ++i) + points.push_back(getPosition(mesh, manager, mesh.face_vertices_[i])); + + const double area = polygonArea(points); + total += area; + oss << "面 " << id << ": " << toString(area) << "\n"; + } + + oss << "总面积: " << toString(total); + return oss.str(); +} + +std::string formatSolidVolume(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const double volume = solidVolume(mesh, manager, id); + if (volume <= 0.0) { + oss << "体 " << id << ": 不支持的体类型或无体积\n"; + continue; + } + total += volume; + oss << "体 " << id << ": " << toString(volume) << "\n"; + } + + oss << "总体积: " << toString(total); + return oss.str(); +} + +std::string formatBoundingBox(const std::vector& positions) +{ + // 防御性判空:防止被其他路径以空容器调用时越界访问首元素 + if (positions.empty()) { + spdlog::error("formatBoundingBox: positions 为空"); + return std::string("错误:没有可用的几何数据"); + } + + Vec3 min = positions[0]; + Vec3 max = positions[0]; + + for (const auto& p : positions) { + for (int i = 0; i < 3; ++i) { + min[i] = std::min(min[i], p[i]); + max[i] = std::max(max[i], p[i]); + } + } + + const Vec3 size = max - min; + + std::ostringstream oss; + oss << "包围盒:\n" + << "min: " << vecString(min) << "\n" + << "max: " << vecString(max) << "\n" + << "size: " << vecString(size); + return oss.str(); +} + +std::string formatCentroid(const std::vector& positions) +{ + // 防御性判空:防止空容器时除零产生 NaN + if (positions.empty()) { + spdlog::error("formatCentroid: positions 为空"); + return std::string("错误:没有可用的几何数据"); + } + + Vec3 sum = { 0.0, 0.0, 0.0 }; + for (const auto& p : positions) + sum = sum + p; + const Vec3 centroid = sum / static_cast(positions.size()); + + std::ostringstream oss; + oss << "重心: " << vecString(centroid); + return oss.str(); +} + +// ---------------- 几何(OCC)测量:选择 id 为 GeometryRegistry 分配的全局几何 id ---------------- + +Vec3 toVec3(const gp_Pnt& p) +{ + return { p.X(), p.Y(), p.Z() }; +} + +//! @brief 按选择类型从几何注册表查找子形状,找不到返回 nullptr +const TopoDS_Shape* findGeometryShape(const GeometryRegistry& reg, ElementEnum::Type type, Index id) +{ + switch (type) { + case ElementEnum::GeometryVertex: + return reg.getVertex(id); + case ElementEnum::GeometryEdge: + return reg.getEdge(id); + case ElementEnum::GeometryFace: + return reg.getFace(id); + case ElementEnum::GeometrySolid: + return reg.getSolid(id); + default: + return nullptr; + } +} + +//! @brief 收集选择中有效的几何子形状 +std::vector collectGeometryShapes(const GeometryRegistry& reg, + ElementEnum::Type type, const std::vector& ids) +{ + std::vector shapes; + shapes.reserve(ids.size()); + for (Index id : ids) { + if (const TopoDS_Shape* s = findGeometryShape(reg, type, id)) + shapes.push_back(*s); + } + return shapes; +} + +//! @brief 取几何点坐标 +bool geometryVertexPoint(const GeometryRegistry& reg, Index id, Vec3& out) +{ + const TopoDS_Shape* s = reg.getVertex(id); + if (!s) + return false; + out = toVec3(BRep_Tool::Pnt(TopoDS::Vertex(*s))); + return true; +} + +//! @brief 取几何边中点处的切向作为边方向 +bool geometryEdgeDirection(const TopoDS_Edge& edge, Vec3& out) +{ + BRepAdaptor_Curve curve(edge); + const double u = 0.5 * (curve.FirstParameter() + curve.LastParameter()); + gp_Pnt p; + gp_Vec v; + curve.D1(u, p, v); + if (v.Magnitude() < std::numeric_limits::epsilon()) + return false; + v.Normalize(); + out = { v.X(), v.Y(), v.Z() }; + return true; +} + +std::string formatGeometryEdgeLength(const GeometryRegistry& reg, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const TopoDS_Shape* s = reg.getEdge(id); + if (!s) { + oss << "边 " << id << ": 无效索引\n"; + continue; + } + GProp_GProps props; + BRepGProp::LinearProperties(*s, props); + const double len = props.Mass(); + total += len; + oss << "边 " << id << ": " << toString(len) << "\n"; + } + + oss << "累计长度: " << toString(total); + return oss.str(); +} + +std::string formatGeometryFaceArea(const GeometryRegistry& reg, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const TopoDS_Shape* s = reg.getFace(id); + if (!s) { + oss << "面 " << id << ": 无效索引\n"; + continue; + } + GProp_GProps props; + BRepGProp::SurfaceProperties(*s, props); + const double area = std::abs(props.Mass()); + total += area; + oss << "面 " << id << ": " << toString(area) << "\n"; + } + + oss << "总面积: " << toString(total); + return oss.str(); +} + +std::string formatGeometrySolidVolume(const GeometryRegistry& reg, const std::vector& ids) +{ + double total = 0.0; + std::ostringstream oss; + + for (Index id : ids) { + const TopoDS_Shape* s = reg.getSolid(id); + if (!s) { + oss << "体 " << id << ": 无效索引\n"; + continue; + } + GProp_GProps props; + BRepGProp::VolumeProperties(*s, props); + const double volume = std::abs(props.Mass()); + total += volume; + oss << "体 " << id << ": " << toString(volume) << "\n"; + } + + oss << "总体积: " << toString(total); + return oss.str(); +} + +std::string formatGeometryBoundingBox(const std::vector& shapes) +{ + Bnd_Box box; + for (const auto& s : shapes) + BRepBndLib::AddOptimal(s, box, false); // 不依赖三角剖分,直接按解析几何求界 + + if (box.IsVoid()) + return std::string("错误:无法计算包围盒"); + + Standard_Real xmin = 0.0, ymin = 0.0, zmin = 0.0, xmax = 0.0, ymax = 0.0, zmax = 0.0; + box.Get(xmin, ymin, zmin, xmax, ymax, zmax); + const Vec3 min { xmin, ymin, zmin }; + const Vec3 max { xmax, ymax, zmax }; + const Vec3 size = max - min; + + std::ostringstream oss; + oss << "包围盒:\n" + << "min: " << vecString(min) << "\n" + << "max: " << vecString(max) << "\n" + << "size: " << vecString(size); + return oss.str(); +} + +//! @brief 按边长 / 面积 / 体积加权的重心 +std::string formatGeometryCentroid(const GeometryRegistry& reg, + ElementEnum::Type type, const std::vector& ids) +{ + double total_mass = 0.0; + Vec3 sum { 0.0, 0.0, 0.0 }; + + for (Index id : ids) { + const TopoDS_Shape* s = findGeometryShape(reg, type, id); + if (!s) + continue; + + GProp_GProps props; + switch (type) { + case ElementEnum::GeometryEdge: + BRepGProp::LinearProperties(*s, props); + break; + case ElementEnum::GeometryFace: + BRepGProp::SurfaceProperties(*s, props); + break; + case ElementEnum::GeometrySolid: + BRepGProp::VolumeProperties(*s, props); + break; + default: + continue; + } + + const double mass = std::abs(props.Mass()); + if (mass < std::numeric_limits::epsilon()) + continue; + sum = sum + toVec3(props.CentreOfMass()) * mass; + total_mass += mass; + } + + if (total_mass < std::numeric_limits::epsilon()) + return std::string("错误:未找到可计算重心的几何对象"); + + std::ostringstream oss; + oss << "重心: " << vecString(sum / total_mass); + return oss.str(); +} + +// ---------------- 几何(OCC)测量实现:选择 id 为 GeometryRegistry 分配的全局几何 id ---------------- + +std::string geomDistance(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometryVertex || selection.ids.size() != 2) { + return std::string("错误:距离测量需要选择两个几何点"); + } + Vec3 a, b; + if (!geometryVertexPoint(reg, selection.ids[0], a) || !geometryVertexPoint(reg, selection.ids[1], b)) + return std::string("错误:无效的几何点 id"); + return formatDistance(a, b); +} + +std::string geomAngle(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type == ElementEnum::GeometryVertex && selection.ids.size() == 3) { + Vec3 a, b, c; + if (!geometryVertexPoint(reg, selection.ids[0], a) + || !geometryVertexPoint(reg, selection.ids[1], b) + || !geometryVertexPoint(reg, selection.ids[2], c)) + return std::string("错误:无效的几何点 id"); + return formatAngle(a, b, c); + } + if (selection.type == ElementEnum::GeometryEdge && selection.ids.size() == 2) { + const TopoDS_Shape* e0 = reg.getEdge(selection.ids[0]); + const TopoDS_Shape* e1 = reg.getEdge(selection.ids[1]); + Vec3 u, v; + if (!e0 || !e1 + || !geometryEdgeDirection(TopoDS::Edge(*e0), u) + || !geometryEdgeDirection(TopoDS::Edge(*e1), v)) + return std::string("错误:无效的几何边 id 或边方向无法计算"); + std::ostringstream oss; + oss << "Angle: " << toString(angleBetween(u, v)) << " deg"; + return oss.str(); + } + return std::string("错误:角度测量需要选择三个几何点或两条几何边"); +} + +std::string geomRadius(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type == ElementEnum::GeometryVertex && selection.ids.size() == 3) { + Vec3 a, b, c; + if (!geometryVertexPoint(reg, selection.ids[0], a) + || !geometryVertexPoint(reg, selection.ids[1], b) + || !geometryVertexPoint(reg, selection.ids[2], c)) + return std::string("错误:无效的几何点 id"); + return formatRadius(a, b, c); + } + if (selection.type == ElementEnum::GeometryEdge && selection.ids.size() == 1) { + const TopoDS_Shape* s = reg.getEdge(selection.ids[0]); + if (!s) + return std::string("错误:无效的几何边 id"); + BRepAdaptor_Curve curve(TopoDS::Edge(*s)); + if (curve.GetType() != GeomAbs_Circle) + return std::string("错误:所选边不是圆弧,无法直接测半径(可改选三个几何点)"); + const gp_Circ circ = curve.Circle(); + std::ostringstream oss; + oss << "Radius: " << toString(circ.Radius()) << "\n" + << "Center: " << vecString(toVec3(circ.Location())); + return oss.str(); + } + return std::string("错误:半径测量需要选择三个几何点或一条圆弧边"); +} + +std::string geomLength(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometryEdge || selection.ids.empty()) { + return std::string("错误:长度测量需要选择几何边"); + } + return formatGeometryEdgeLength(reg, selection.ids); +} + +std::string geomArea(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometryFace || selection.ids.empty()) { + return std::string("错误:面积测量需要选择几何面"); + } + return formatGeometryFaceArea(reg, selection.ids); +} + +std::string geomVolume(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type != ElementEnum::GeometrySolid || selection.ids.empty()) { + return std::string("错误:体积测量需要选择几何体"); + } + return formatGeometrySolidVolume(reg, selection.ids); +} + +std::string geomBoundingBox(const GeometryRegistry& reg, const Selection& selection) +{ + const auto shapes = collectGeometryShapes(reg, selection.type, selection.ids); + if (shapes.empty()) { + return std::string("错误:未找到有效的几何对象"); + } + return formatGeometryBoundingBox(shapes); +} + +std::string geomCentroid(const GeometryRegistry& reg, const Selection& selection) +{ + if (selection.type == ElementEnum::GeometryVertex) { + std::vector points; + points.reserve(selection.ids.size()); + for (Index id : selection.ids) { + Vec3 p; + if (geometryVertexPoint(reg, id, p)) + points.push_back(p); + } + if (points.empty()) + return std::string("错误:未找到有效的几何点"); + return formatCentroid(points); + } + if (selection.ids.empty()) { + return std::string("错误:未找到有效的几何对象"); + } + return formatGeometryCentroid(reg, selection.type, selection.ids); +} + +// ---------------- 网格测量实现 ---------------- + +std::string meshDistance(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Vertex || selection.ids.size() != 2) { + spdlog::error("DimensionHandler::execute: 距离测量需要选择两个点"); + return std::string("错误:距离测量需要选择两个点"); + } + const Vec3& a = getPosition(mesh, manager, selection.ids[0]); + const Vec3& b = getPosition(mesh, manager, selection.ids[1]); + return formatDistance(a, b); +} + +std::string meshAngle(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type == ElementEnum::Vertex && selection.ids.size() == 3) { + const Vec3& a = getPosition(mesh, manager, selection.ids[0]); + const Vec3& b = getPosition(mesh, manager, selection.ids[1]); + const Vec3& c = getPosition(mesh, manager, selection.ids[2]); + return formatAngle(a, b, c); + } + if (selection.type == ElementEnum::Edge && selection.ids.size() == 4) { + // 边选择的 ids 是每条边两个端点的顶点 id(EdgeSelectorHighlight 约定) + const auto& ids = selection.ids; + const bool valid = isValidVertexId(mesh, manager, ids[0]) && isValidVertexId(mesh, manager, ids[1]) + && isValidVertexId(mesh, manager, ids[2]) && isValidVertexId(mesh, manager, ids[3]); + if (valid) { + const Vec3 u = getPosition(mesh, manager, ids[1]) - getPosition(mesh, manager, ids[0]); + const Vec3 v = getPosition(mesh, manager, ids[3]) - getPosition(mesh, manager, ids[2]); + std::ostringstream oss; + oss << "Angle: " << toString(angleBetween(u, v)) << " deg"; + return oss.str(); + } + } + spdlog::error("DimensionHandler::execute: 角度测量需要选择三个点或两条边"); + return std::string("错误:角度测量需要选择三个点或两条边"); +} + +std::string meshRadius(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Vertex || selection.ids.size() != 3) { + spdlog::error("DimensionHandler::execute: 半径测量需要选择三个点"); + return std::string("错误:半径测量需要选择三个点"); + } + const Vec3& a = getPosition(mesh, manager, selection.ids[0]); + const Vec3& b = getPosition(mesh, manager, selection.ids[1]); + const Vec3& c = getPosition(mesh, manager, selection.ids[2]); + return formatRadius(a, b, c); +} + +std::string meshLength(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + // 边选择的 ids 是每条边两个端点的顶点 id 对,数量应为正偶数 + if (selection.type != ElementEnum::Edge || selection.ids.empty() || selection.ids.size() % 2 != 0) { + spdlog::error("DimensionHandler::execute: 长度测量需要选择边"); + return std::string("错误:长度测量需要选择边"); + } + return formatEdgeLength(mesh, manager, selection.ids); +} + +std::string meshArea(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Face) { + spdlog::error("DimensionHandler::execute: 面积测量需要选择面"); + return std::string("错误:面积测量需要选择面"); + } + return formatFaceArea(mesh, manager, selection.ids); +} + +std::string meshVolume(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + if (selection.type != ElementEnum::Solid) { + spdlog::error("DimensionHandler::execute: 体积测量需要选择体"); + return std::string("错误:体积测量需要选择体"); + } + return formatSolidVolume(mesh, manager, selection.ids); +} + +std::string meshBoundingBox(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + const auto positions = collectPositions(mesh, manager, selection.type, selection.ids); + if (positions.empty()) { + spdlog::error("DimensionHandler::execute: 未找到可用顶点"); + return std::string("错误:未找到可用顶点"); + } + return formatBoundingBox(positions); +} + +std::string meshCentroid(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) +{ + const auto positions = collectPositions(mesh, manager, selection.type, selection.ids); + if (positions.empty()) { + spdlog::error("DimensionHandler::execute: 未找到可用顶点"); + return std::string("错误:未找到可用顶点"); + } + return formatCentroid(positions); +} + +//! @brief 测量操作注册表:新增测量类型只需在此加一行,args_type/校验/分发全部由表生成 +const MeasureOp kMeasureOps[] = { + { MeasureType::Distance, "距离", "Vertex", "GeometryVertex", meshDistance, geomDistance }, + { MeasureType::Angle, "角度", "Vertex,Edge", "GeometryVertex,GeometryEdge", meshAngle, geomAngle }, + { MeasureType::Radius, "半径", "Vertex", "GeometryVertex,GeometryEdge", meshRadius, geomRadius }, + { MeasureType::Length, "长度", "Edge", "GeometryEdge", meshLength, geomLength }, + { MeasureType::Area, "面积", "Face", "GeometryFace", meshArea, geomArea }, + { MeasureType::Volume, "体积", "Solid", "GeometrySolid", meshVolume, geomVolume }, + { MeasureType::BoundingBox, "包围盒", "Vertex,Edge,Face,Solid", "GeometryVertex,GeometryEdge,GeometryFace,GeometrySolid", meshBoundingBox, geomBoundingBox }, + { MeasureType::Centroid, "重心", "Vertex,Edge,Face,Solid", "GeometryVertex,GeometryEdge,GeometryFace,GeometrySolid", meshCentroid, geomCentroid }, +}; + +//! @brief 按下标查注册表,越界返回 nullptr +const MeasureOp* findMeasureOp(int index) +{ + if (index < 0 || index >= static_cast(sizeof(kMeasureOps) / sizeof(kMeasureOps[0]))) + return nullptr; + return &kMeasureOps[index]; +} + +//! @brief 由注册表生成 Combo 内容(名称与下标天然一致) +std::string comboContent() +{ + std::string out; + for (const MeasureOp& op : kMeasureOps) { + if (!out.empty()) + out += ","; + out += op.name; + } + return out; +} + +//! @brief 由注册表生成选择器内容:网格模式按 Vertex,Edge,Face,Solid 规范序取并集 +std::string selectorContent() +{ + static const char* canonical[] = { "Vertex", "Edge", "Face", "Solid" }; + std::string out; + for (const char* mode : canonical) { + bool used = false; + for (const MeasureOp& op : kMeasureOps) { + if (std::string(op.mesh_selector_modes).find(mode) != std::string::npos) { + used = true; + break; + } + } + if (used) { + if (!out.empty()) + out += ","; + out += mode; + } + } + return out; +} +} + +void DimensionHandler::setup(FeatureRegistrar& reg) +{ + // Combo 内容全部由测量操作注册表生成,名称与下标天然一致 + reg.addParameter({ ArgTypeEnum::Combo, "测量类型", comboContent() + "|0" }); + reg.addParameter({ ArgTypeEnum::Selector, "选择对象", selectorContent() }); + reg.addMenuItem({ "工具", "尺寸标注" }); +} + +std::any DimensionHandler::execute(FeatureContext& ctx) +{ + const int* type_index = ctx.params.value(kTypeParam).get(); + if (!type_index) { + spdlog::error("DimensionHandler::execute: 测量类型参数无效"); + return std::string("错误:测量类型参数无效"); + } + + const auto selection_ptr = ctx.params.value(kSelectionParam).get(); + if (!selection_ptr || !*selection_ptr) { + spdlog::error("DimensionHandler::execute: 选择对象参数无效"); + return std::string("错误:选择对象参数无效"); + } + + const auto& selection = **selection_ptr; + + // 选择集未带组件 id 时回退当前活动组件 + Index selected_component_id = selection.component_id; + if (selected_component_id < 0) { + const auto active_component = ctx.activeComponent ? ctx.activeComponent() : std::nullopt; + if (active_component) { + selected_component_id = *active_component; + } + } + ComponentData* comp = ctx.model.findComponent(selected_component_id); + if (!comp) { + spdlog::error("DimensionHandler::execute: 找不到选择对象所在的组件"); + return std::string("错误:找不到选择对象所在的组件"); + } + ModelLayer& manager = ctx.model; + const MeasureOp* op = findMeasureOp(*type_index); + if (!op) { + spdlog::error("DimensionHandler::execute: 未知测量类型下标 {}", *type_index); + return std::string("错误:未知测量类型"); + } + + MeshData* mesh = comp->asMeshData(); + if (!mesh) { + // 无网格但有几何(STEP/IGES):走 OCC 几何测量 + if (comp->geometry) { + comp->geometry->ensureIndexBuilt(manager.geomRegistry()); + if (!op->geom_exec) + return std::string("错误:") + op->name + "测量暂不支持几何模型"; + return op->geom_exec(manager.geomRegistry(), selection); + } + spdlog::error("DimensionHandler::execute: 选择对象所在组件没有网格或几何数据"); + return std::string("错误:选择对象所在组件没有网格或几何数据"); + } + + return op->mesh_exec(*mesh, manager, selection); +} +} // namespace systems::feature diff --git a/plugins/feature/DimensionPlugin/DimensionHandler.h b/plugins/feature/DimensionPlugin/DimensionHandler.h new file mode 100644 index 0000000..1cf9332 --- /dev/null +++ b/plugins/feature/DimensionPlugin/DimensionHandler.h @@ -0,0 +1,27 @@ +/** + * @file DimensionHandler.h + * @brief 尺寸标注处理器声明:距离、角度、半径、长度、面积、体积、包围盒与重心 + * @author 范成通 email 1941804585@qq.com + */ +#pragma once +#include "FeatureHandler.h" + +namespace systems::feature { + +/** + * @brief 尺寸标注处理器:功能参数(测量类型 + 选择对象)+ 菜单执行 → 返回结果文本 + * + * 纯参数执行功能:setup()/execute() 均在 GUI 线程调用,execute 无状态, + * 与视口交互测量(MeasurePlugin)相互独立。 + */ +class DimensionHandler : public FeatureHandler { +public: + DimensionHandler() = default; + ~DimensionHandler() override = default; + + //! @brief 声明功能参数(测量类型、选择对象)与菜单项 + void setup(FeatureRegistrar& reg) override; + //! @brief 尺寸标注:按参数中的测量类型与选择对象执行,返回结果文本 + std::any execute(FeatureContext& ctx) override; +}; +} diff --git a/plugins/feature/DimensionPlugin/DimensionPlugin.h b/plugins/feature/DimensionPlugin/DimensionPlugin.h new file mode 100644 index 0000000..a913086 --- /dev/null +++ b/plugins/feature/DimensionPlugin/DimensionPlugin.h @@ -0,0 +1,24 @@ +/** + * @file DimensionPlugin.h + * @brief 尺寸标注插件声明:将 DimensionHandler 注册到功能系统 + * @author 范成通 email 1941804585@qq.com + */ +#pragma once +#include "DimensionHandler.h" +#include "HandlerCreatorDestroyerFactory.h" +#include "PluginBase.h" + +#include + +namespace systems::feature { +class DimensionPlugin : public QObject, public PluginBase { + Q_OBJECT + Q_INTERFACES(systems::PluginBase) + Q_PLUGIN_METADATA(IID "com.PreCess.systems.feature.DimensionPlugin/1.0" FILE "DimensionPlugin.json") +private: + const HandlerCreatorDestroyer& getHandlerCreatorDestroyer() noexcept override final + { + return HandlerCreatorDestroyerFactory::get(); + } +}; +} diff --git a/plugins/feature/DimensionPlugin/DimensionPlugin.json b/plugins/feature/DimensionPlugin/DimensionPlugin.json new file mode 100644 index 0000000..9eadbd4 --- /dev/null +++ b/plugins/feature/DimensionPlugin/DimensionPlugin.json @@ -0,0 +1,8 @@ +{ + "system": "FeatureSystem", + "handler": { + "name": "DimensionPlugin", + "display_name": "尺寸标注", + "description": "参数化尺寸标注:按测量类型(距离/角度/半径/长度/面积/体积/包围盒/重心)与选择对象执行并返回结果文本" + } +} diff --git a/plugins/feature/DimensionPlugin/test/CMakeLists.txt b/plugins/feature/DimensionPlugin/test/CMakeLists.txt new file mode 100644 index 0000000..b16907e --- /dev/null +++ b/plugins/feature/DimensionPlugin/test/CMakeLists.txt @@ -0,0 +1,2 @@ +precess_add_test(TestDimensionHandler TestDimensionHandler.cpp) +precess_test_link_libraries(TestDimensionHandler DimensionPluginlib DataTest TKPrim TKBRep TKTopAlgo TKernel) diff --git a/plugins/feature/DimensionPlugin/test/TestDimensionHandler.cpp b/plugins/feature/DimensionPlugin/test/TestDimensionHandler.cpp new file mode 100644 index 0000000..6d98520 --- /dev/null +++ b/plugins/feature/DimensionPlugin/test/TestDimensionHandler.cpp @@ -0,0 +1,268 @@ +/** + * @file TestDimensionHandler.cpp + * @brief 尺寸标注处理器的单元测试 + * @author 范成通 email 1941804585@qq.com + */ + +#include "DimensionHandler.h" +#include "ArgObject.h" +#include "ComponentData.h" +#include "EventBus.h" +#include "FeatureContext.h" +#include "FeatureParams.h" +#include "FeatureRegistrar.h" +#include "GeometryData.h" +#include "InteractionContext.h" +#include "InteractionState.h" +#include "MakeMeshData.h" +#include "MeshData.h" +#include "ModelLayer.h" +#include "Selection.h" + +#include +#include + +#include + +#include +#include +#include +#include + +using namespace systems::feature; + +namespace { + +std::shared_ptr makeVertexSelection(const std::vector& ids) +{ + return std::make_shared(Selection { ids, ElementEnum::Vertex, 0 }); +} + +std::shared_ptr makeEdgeSelection(const std::vector& ids) +{ + return std::make_shared(Selection { ids, ElementEnum::Edge, 0 }); +} + +std::shared_ptr makeGeometrySelection(ElementEnum::Type type, const std::vector& ids) +{ + return std::make_shared(Selection { ids, type, 0 }); +} + +//! @brief 功能测试环境:手工装配 FeatureContext(参数集 + 活动组件 provider),替代原算法 HandlerContext +struct FeatureTestEnv { + ModelLayer mgr; + core::EventBus bus; + DimensionHandler handler; + FeatureRegistrar registrar; + std::optional active_component; + std::unique_ptr params; + systems::interaction::InteractionState interaction_state_; + InteractionContext interaction_ctx_; + std::unique_ptr ctx; + + FeatureTestEnv() + : interaction_ctx_(interaction_state_) + { + // 参数声明直接取自被测功能的 setup,避免测试中重复维护一份 + handler.setup(registrar); + params = std::make_unique(registrar.argTypes()); + ctx = std::make_unique(FeatureContext { + mgr, + bus, + *params, + interaction_ctx_, + []() -> std::optional { return std::nullopt; }, + [this]() { return active_component; }, + [this](Index component_id) { return mgr.getComponentOperator(component_id); }, + }); + } + + //! @brief 写入功能参数并执行尺寸标注,返回结果字符串(下标 0=测量类型 1=选择对象,见 setup) + std::string executeDimension(int measure_type_index, std::shared_ptr selection) + { + params->setValue(0, core::ArgObject::create(measure_type_index)); + params->setValue(1, core::ArgObject::create(std::move(selection))); + auto result_any = handler.execute(*ctx); + if (const std::string* result = std::any_cast(&result_any)) + return *result; + return {}; + } +}; + +//! @brief 在 10×10×10 的 OCC 立方体几何组件上执行一次测量,返回结果字符串 +std::string executeGeometryMeasureOnBox(int measure_type_index, ElementEnum::Type sel_type, int local_id) +{ + BRepPrimAPI_MakeBox box_maker(10.0, 10.0, 10.0); + box_maker.Build(); + if (!box_maker.IsDone()) + return {}; + + auto geometry = std::make_unique(); + geometry->rootShape = std::make_unique(box_maker.Shape()); + + auto c = std::make_unique(); + c->id = -1; + c->name = "box"; + c->geometry = std::move(geometry); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("box", std::move(comps)); + auto cids = env.mgr.modelById(model_id)->componentIds(); + if (cids.size() != 1) + return {}; + + ComponentData* comp = env.mgr.findComponent(cids[0]); + if (!comp) + return {}; + comp->geometry->ensureIndexBuilt(env.mgr.geomRegistry()); + + Index gid = -1; + switch (sel_type) { + case ElementEnum::GeometryEdge: + gid = comp->geometry->index.edgeGlobalId(local_id); + break; + case ElementEnum::GeometryFace: + gid = comp->geometry->index.faceGlobalId(local_id); + break; + case ElementEnum::GeometrySolid: + gid = comp->geometry->index.solidGlobalId(local_id); + break; + default: + break; + } + + auto selection = makeGeometrySelection(sel_type, { gid }); + selection->component_id = cids[0]; + return env.executeDimension(measure_type_index, selection); +} + +} // namespace + +TEST_CASE("DimensionHandler setup declares measure parameters and menu") +{ + DimensionHandler handler; + FeatureRegistrar reg; + handler.setup(reg); + + REQUIRE(reg.argTypes().size() == 2); + CHECK(reg.argTypes()[0].type == ArgTypeEnum::Combo); + CHECK(reg.argTypes()[1].type == ArgTypeEnum::Selector); + REQUIRE(reg.menuItems().size() == 1); +} + +TEST_CASE("DimensionHandler: distance between two vertices") +{ + auto mesh = std::make_unique(MakeMeshData()); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("test", std::move(comps)); + + auto cids = env.mgr.modelById(model_id)->componentIds(); + REQUIRE(cids.size() == 1); + + ComponentData* comp = env.mgr.findComponent(cids[0]); + REQUIRE(comp); + + MeshData* mesh_ptr = comp->asMeshData(); + REQUIRE(mesh_ptr); + const Index base = mesh_ptr->local_to_global_[0]; + + auto selection = makeVertexSelection({ base + 0, base + 1 }); + selection->component_id = cids[0]; + const std::string result = env.executeDimension(0, selection); // 距离 + CHECK(result.find("Distance") != std::string::npos); + CHECK(result.find("1.000000") != std::string::npos); +} + +TEST_CASE("DimensionHandler: length of edges selected as endpoint vertex id pairs") +{ + auto mesh = std::make_unique(MakeMeshData()); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("test", std::move(comps)); + + auto cids = env.mgr.modelById(model_id)->componentIds(); + REQUIRE(cids.size() == 1); + + ComponentData* comp = env.mgr.findComponent(cids[0]); + REQUIRE(comp); + + MeshData* mesh_ptr = comp->asMeshData(); + REQUIRE(mesh_ptr); + const Index base = mesh_ptr->local_to_global_[0]; + + // 边选择的 ids 是端点顶点 id 对:{12,13} 与 {0,1} 长度均为 1 + auto selection = makeEdgeSelection({ base + 12, base + 13, base + 0, base + 1 }); + selection->component_id = cids[0]; + const std::string result = env.executeDimension(3, selection); // 长度 + CHECK(result.find("累计长度: 2.000000") != std::string::npos); +} + +TEST_CASE("DimensionHandler: angle between two edges selected as endpoint vertex id pairs") +{ + auto mesh = std::make_unique(MakeMeshData()); + auto c = std::make_unique(); + c->id = -1; + c->name = "Comp"; + c->mesh = std::move(mesh); + + ComponentDatas comps; + comps.push_back(std::move(c)); + + FeatureTestEnv env; + const Index model_id = env.mgr.addModel("test", std::move(comps)); + + auto cids = env.mgr.modelById(model_id)->componentIds(); + REQUIRE(cids.size() == 1); + + ComponentData* comp = env.mgr.findComponent(cids[0]); + REQUIRE(comp); + + MeshData* mesh_ptr = comp->asMeshData(); + REQUIRE(mesh_ptr); + const Index base = mesh_ptr->local_to_global_[0]; + + // 边 {0,1} 方向 (1,0,0),边 {0,3} 方向 (0,1,0),夹角 90° + auto selection = makeEdgeSelection({ base + 0, base + 1, base + 0, base + 3 }); + selection->component_id = cids[0]; + const std::string result = env.executeDimension(1, selection); // 角度 + CHECK(result.find("Angle: 90.000000 deg") != std::string::npos); +} + +TEST_CASE("DimensionHandler: geometry edge length on OCC box") +{ + // 10×10×10 立方体任一条几何边长度均为 10 + const std::string result = executeGeometryMeasureOnBox(3, ElementEnum::GeometryEdge, 1); + CHECK(result.find("累计长度: 10.000000") != std::string::npos); +} + +TEST_CASE("DimensionHandler: geometry face area on OCC box") +{ + // 10×10×10 立方体任一个几何面面积均为 100 + const std::string result = executeGeometryMeasureOnBox(4, ElementEnum::GeometryFace, 1); + CHECK(result.find("总面积: 100.000000") != std::string::npos); +} + +TEST_CASE("DimensionHandler: geometry solid volume on OCC box") +{ + // 10×10×10 立方体体积为 1000 + const std::string result = executeGeometryMeasureOnBox(5, ElementEnum::GeometrySolid, 1); + CHECK(result.find("总体积: 1000.000000") != std::string::npos); +} diff --git a/plugins/feature/MeasurePlugin/CMakeLists.txt b/plugins/feature/MeasurePlugin/CMakeLists.txt index c2c6568..a6cffa4 100644 --- a/plugins/feature/MeasurePlugin/CMakeLists.txt +++ b/plugins/feature/MeasurePlugin/CMakeLists.txt @@ -3,10 +3,4 @@ precess_add_feature_plugin(MeasurePlugin PLUGIN_H "MeasurePlugin.h" ) -precess_plugin_link_libraries(MeasurePlugin - TKernel # Standard / gp 基础类型 - TKBRep # BRep_Tool、BRepAdaptor - TKTopAlgo # BRepGProp、BRepBndLib -) - add_subdirectory(test) diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.cpp b/plugins/feature/MeasurePlugin/MeasureHandler.cpp index 2a9faf6..4a746fc 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/MeasureHandler.cpp @@ -1,44 +1,18 @@ /** * @file MeasureHandler.cpp - * @brief 网格测量处理器,支持距离、角度、半径、长度、面积、体积、包围盒与重心 + * @brief 测量处理器:交互式两点成线测距与共端点夹角标注 * @author 范成通 email 1941804585@qq.com */ #include "MeasureHandler.h" -#include "ArgObject.h" -#include "ComponentData.h" #include "FeatureContext.h" -#include "FeatureParams.h" #include "FeatureRegistrar.h" -#include "GeometryData.h" -#include "GeometryRegistry.h" #include "InteractionContext.h" -#include "MeshData.h" -#include "ModelLayer.h" -#include "Selection.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include #include #include -#include #include #include -#include #include #include @@ -47,30 +21,13 @@ namespace systems::feature { namespace { using Vec3 = std::array; -// 参数下标:测量类型 / 选择对象(与 setup() 注册顺序一致) -constexpr std::size_t kTypeParam = 0; -constexpr std::size_t kSelectionParam = 1; +constexpr double kEps = 1e-9; Vec3 operator-(const Vec3& a, const Vec3& b) { return { a[0] - b[0], a[1] - b[1], a[2] - b[2] }; } -Vec3 operator+(const Vec3& a, const Vec3& b) -{ - return { a[0] + b[0], a[1] + b[1], a[2] + b[2] }; -} - -Vec3 operator*(const Vec3& a, double s) -{ - return { a[0] * s, a[1] * s, a[2] * s }; -} - -Vec3 operator/(const Vec3& a, double s) -{ - return { a[0] / s, a[1] / s, a[2] / s }; -} - //! @brief 两点中点 Vec3 midpoint(const Vec3& a, const Vec3& b) { @@ -87,57 +44,6 @@ double length(const Vec3& v) return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); } -Vec3 cross(const Vec3& a, const Vec3& b) -{ - return { - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0] - }; -} - -enum class MeasureType { - Distance, - Angle, - Radius, - Length, - Area, - Volume, - BoundingBox, - Centroid -}; - -//! @brief 网格测量实现签名 -using MeshExecFn = std::string (*)(const MeshData&, const ModelLayer&, const Selection&); -//! @brief 几何测量实现签名 -using GeomExecFn = std::string (*)(const GeometryRegistry&, const Selection&); - -//! @brief 测量操作表项:名称、选择器模式与两套实现的统一注册处(新增类型只需在 kMeasureOps 加一行) -struct MeasureOp { - MeasureType type; - const char* name; //> Combo 显示名(表内顺序即下标) - const char* mesh_selector_modes; //> 网格组件的选择器模式 - const char* geom_selector_modes; //> 几何组件的选择器模式 - MeshExecFn mesh_exec; //> 网格实现 - GeomExecFn geom_exec; //> 几何实现(nullptr 表示暂不支持几何模型) -}; - -bool isValidVertexId(const MeshData& mesh, const ModelLayer& manager, Index id) -{ - if (id < 0) - return false; - if (mesh.local_to_global_.empty()) - return id < static_cast(mesh.vertex_positions_.size()); - return id < static_cast(manager.globalPoints().size()); -} - -const Vec3& getPosition(const MeshData& mesh, const ModelLayer& manager, Index id) -{ - if (!mesh.local_to_global_.empty() && !manager.globalPoints().empty()) - return manager.globalPoints()[id]; - return mesh.vertex_positions_[id]; -} - std::string toString(double value, int precision = 6) { std::ostringstream oss; @@ -147,11 +53,6 @@ std::string toString(double value, int precision = 6) return oss.str(); } -std::string vecString(const Vec3& v) -{ - return "(" + toString(v[0]) + ", " + toString(v[1]) + ", " + toString(v[2]) + ")"; -} - double angleBetween(const Vec3& u, const Vec3& v) { const double lu = length(u); @@ -163,811 +64,15 @@ double angleBetween(const Vec3& u, const Vec3& v) cos_theta = std::max(-1.0, std::min(1.0, cos_theta)); return std::acos(cos_theta) * 180.0 / 3.14159265358979323846; } - -double triangleArea(const Vec3& a, const Vec3& b, const Vec3& c) -{ - return 0.5 * length(cross(b - a, c - a)); -} - -double polygonArea(const std::vector& points) -{ - const size_t n = points.size(); - if (n < 3) - return 0.0; - - double area = 0.0; - for (size_t i = 2; i < n; ++i) - area += triangleArea(points[0], points[i - 1], points[i]); - return area; -} - -double tetraVolume(const Vec3& a, const Vec3& b, const Vec3& c, const Vec3& d) -{ - return std::abs(dot(b - a, cross(c - a, d - a))) / 6.0; -} - -double polyhedronVolume(const MeshData& mesh, const ModelLayer& manager, Index solid_id) -{ - if (solid_id + 1 >= static_cast(mesh.solid_faces_offset_.size())) - return 0.0; - - const Index face_start = mesh.solid_faces_offset_[solid_id]; - const Index face_end = mesh.solid_faces_offset_[solid_id + 1]; - double volume = 0.0; - - for (Index f = face_start; f < face_end; ++f) { - const Index face_id = mesh.solid_faces_[f]; - if (face_id + 1 >= static_cast(mesh.solid_faces_vertices_offset_.size())) - continue; - - const Index vert_start = mesh.solid_faces_vertices_offset_[face_id]; - const Index vert_end = mesh.solid_faces_vertices_offset_[face_id + 1]; - if (vert_end - vert_start < 3) - continue; - - const Vec3& v0 = getPosition(mesh, manager, mesh.solid_faces_vertices_[vert_start]); - for (Index i = vert_start + 1; i + 1 < vert_end; ++i) { - const Vec3& vi = getPosition(mesh, manager, mesh.solid_faces_vertices_[i]); - const Vec3& vj = getPosition(mesh, manager, mesh.solid_faces_vertices_[i + 1]); - volume += dot(v0, cross(vi, vj)) / 6.0; - } - } - - return std::abs(volume); -} - -double solidVolume(const MeshData& mesh, const ModelLayer& manager, Index solid_id) -{ - if (solid_id < 0 || solid_id + 1 >= static_cast(mesh.solid_vertices_offset_.size())) - return 0.0; - if (solid_id >= static_cast(mesh.solid_types_.size())) - return 0.0; - - const unsigned char cell_type = mesh.solid_types_[solid_id]; - const Index start = mesh.solid_vertices_offset_[solid_id]; - const Index end = mesh.solid_vertices_offset_[solid_id + 1]; - - // VTK_TETRA = 10 - if (cell_type == 10) { - if (end - start != 4) - return 0.0; - const Vec3& a = getPosition(mesh, manager, mesh.solid_vertices_[start]); - const Vec3& b = getPosition(mesh, manager, mesh.solid_vertices_[start + 1]); - const Vec3& c = getPosition(mesh, manager, mesh.solid_vertices_[start + 2]); - const Vec3& d = getPosition(mesh, manager, mesh.solid_vertices_[start + 3]); - return tetraVolume(a, b, c, d); - } - - // VTK_POLYHEDRON = 42 - if (cell_type == 42) - return polyhedronVolume(mesh, manager, solid_id); - - return 0.0; -} - -std::vector collectPositions(const MeshData& mesh, const ModelLayer& manager, - ElementEnum::Type type, const std::vector& ids) -{ - std::vector positions; - positions.reserve(ids.size() * 4); - - switch (type) { - case ElementEnum::Vertex: - case ElementEnum::Edge: - // 边选择的 ids 即每条边端点的顶点 id(EdgeSelectorHighlight 约定),与点一样按顶点处理 - for (Index id : ids) { - if (isValidVertexId(mesh, manager, id)) - positions.push_back(getPosition(mesh, manager, id)); - } - break; - case ElementEnum::Face: - for (Index id : ids) { - if (id + 1 >= static_cast(mesh.face_vertices_offset_.size())) - continue; - for (Index i = mesh.face_vertices_offset_[id]; i < mesh.face_vertices_offset_[id + 1]; ++i) { - const Index v = mesh.face_vertices_[i]; - if (isValidVertexId(mesh, manager, v)) - positions.push_back(getPosition(mesh, manager, v)); - } - } - break; - case ElementEnum::Solid: - for (Index id : ids) { - if (id + 1 >= static_cast(mesh.solid_vertices_offset_.size())) - continue; - for (Index i = mesh.solid_vertices_offset_[id]; i < mesh.solid_vertices_offset_[id + 1]; ++i) { - const Index v = mesh.solid_vertices_[i]; - if (isValidVertexId(mesh, manager, v)) - positions.push_back(getPosition(mesh, manager, v)); - } - } - break; - default: - break; - } - - return positions; -} - -std::string formatDistance(const Vec3& a, const Vec3& b) -{ - const Vec3 d = b - a; - std::ostringstream oss; - oss << "Distance: " << toString(length(d)) << "\n" - << "Dx: " << toString(d[0]) << "\n" - << "Dy: " << toString(d[1]) << "\n" - << "Dz: " << toString(d[2]); - return oss.str(); -} - -std::string formatAngle(const Vec3& a, const Vec3& b, const Vec3& c) -{ - std::ostringstream oss; - oss << "Angle: " << toString(angleBetween(a - b, c - b)) << " deg"; - return oss.str(); -} - -std::string formatRadius(const Vec3& a, const Vec3& b, const Vec3& c) -{ - const Vec3 ab = b - a; - const Vec3 ac = c - a; - const Vec3 n = cross(ab, ac); - const double n_len2 = dot(n, n); - - if (n_len2 < std::numeric_limits::epsilon()) - return "错误:三点共线或重合,无法计算半径"; - - const Vec3 circumcenter = a + (cross(ac, n) * dot(ab, ab) - cross(ab, n) * dot(ac, ac)) / (2.0 * n_len2); - const double radius = length(circumcenter - a); - - std::ostringstream oss; - oss << "Radius: " << toString(radius) << "\n" - << "Center: " << vecString(circumcenter); - return oss.str(); -} - -std::string formatEdgeLength(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) -{ - double total = 0.0; - std::ostringstream oss; - - // 边选择的 ids 是每条边两个端点的顶点 id 对,逐对计算长度 - for (size_t i = 0; i + 1 < ids.size(); i += 2) { - const Index v0 = ids[i]; - const Index v1 = ids[i + 1]; - const size_t edge_no = i / 2; - if (!isValidVertexId(mesh, manager, v0) || !isValidVertexId(mesh, manager, v1)) { - oss << "边 " << edge_no << ": 无效顶点\n"; - continue; - } - - const double len = length(getPosition(mesh, manager, v1) - getPosition(mesh, manager, v0)); - total += len; - oss << "边 " << edge_no << ": " << toString(len) << "\n"; - } - - oss << "累计长度: " << toString(total); - return oss.str(); -} - -std::string formatFaceArea(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) -{ - double total = 0.0; - std::ostringstream oss; - - for (Index id : ids) { - if (id + 1 >= static_cast(mesh.face_vertices_offset_.size())) { - oss << "面 " << id << ": 无效索引\n"; - continue; - } - - std::vector points; - for (Index i = mesh.face_vertices_offset_[id]; i < mesh.face_vertices_offset_[id + 1]; ++i) - points.push_back(getPosition(mesh, manager, mesh.face_vertices_[i])); - - const double area = polygonArea(points); - total += area; - oss << "面 " << id << ": " << toString(area) << "\n"; - } - - oss << "总面积: " << toString(total); - return oss.str(); -} - -std::string formatSolidVolume(const MeshData& mesh, const ModelLayer& manager, const std::vector& ids) -{ - double total = 0.0; - std::ostringstream oss; - - for (Index id : ids) { - const double volume = solidVolume(mesh, manager, id); - if (volume <= 0.0) { - oss << "体 " << id << ": 不支持的体类型或无体积\n"; - continue; - } - total += volume; - oss << "体 " << id << ": " << toString(volume) << "\n"; - } - - oss << "总体积: " << toString(total); - return oss.str(); -} - -std::string formatBoundingBox(const std::vector& positions) -{ - // 防御性判空:防止被其他路径以空容器调用时越界访问首元素 - if (positions.empty()) { - spdlog::error("formatBoundingBox: positions 为空"); - return std::string("错误:没有可用的几何数据"); - } - - Vec3 min = positions[0]; - Vec3 max = positions[0]; - - for (const auto& p : positions) { - for (int i = 0; i < 3; ++i) { - min[i] = std::min(min[i], p[i]); - max[i] = std::max(max[i], p[i]); - } - } - - const Vec3 size = max - min; - - std::ostringstream oss; - oss << "包围盒:\n" - << "min: " << vecString(min) << "\n" - << "max: " << vecString(max) << "\n" - << "size: " << vecString(size); - return oss.str(); -} - -std::string formatCentroid(const std::vector& positions) -{ - // 防御性判空:防止空容器时除零产生 NaN - if (positions.empty()) { - spdlog::error("formatCentroid: positions 为空"); - return std::string("错误:没有可用的几何数据"); - } - - Vec3 sum = { 0.0, 0.0, 0.0 }; - for (const auto& p : positions) - sum = sum + p; - const Vec3 centroid = sum / static_cast(positions.size()); - - std::ostringstream oss; - oss << "重心: " << vecString(centroid); - return oss.str(); -} - -// ---------------- 几何(OCC)测量:选择 id 为 GeometryRegistry 分配的全局几何 id ---------------- - -Vec3 toVec3(const gp_Pnt& p) -{ - return { p.X(), p.Y(), p.Z() }; -} - -//! @brief 按选择类型从几何注册表查找子形状,找不到返回 nullptr -const TopoDS_Shape* findGeometryShape(const GeometryRegistry& reg, ElementEnum::Type type, Index id) -{ - switch (type) { - case ElementEnum::GeometryVertex: - return reg.getVertex(id); - case ElementEnum::GeometryEdge: - return reg.getEdge(id); - case ElementEnum::GeometryFace: - return reg.getFace(id); - case ElementEnum::GeometrySolid: - return reg.getSolid(id); - default: - return nullptr; - } -} - -//! @brief 收集选择中有效的几何子形状 -std::vector collectGeometryShapes(const GeometryRegistry& reg, - ElementEnum::Type type, const std::vector& ids) -{ - std::vector shapes; - shapes.reserve(ids.size()); - for (Index id : ids) { - if (const TopoDS_Shape* s = findGeometryShape(reg, type, id)) - shapes.push_back(*s); - } - return shapes; -} - -//! @brief 取几何点坐标 -bool geometryVertexPoint(const GeometryRegistry& reg, Index id, Vec3& out) -{ - const TopoDS_Shape* s = reg.getVertex(id); - if (!s) - return false; - out = toVec3(BRep_Tool::Pnt(TopoDS::Vertex(*s))); - return true; -} - -//! @brief 取几何边中点处的切向作为边方向 -bool geometryEdgeDirection(const TopoDS_Edge& edge, Vec3& out) -{ - BRepAdaptor_Curve curve(edge); - const double u = 0.5 * (curve.FirstParameter() + curve.LastParameter()); - gp_Pnt p; - gp_Vec v; - curve.D1(u, p, v); - if (v.Magnitude() < std::numeric_limits::epsilon()) - return false; - v.Normalize(); - out = { v.X(), v.Y(), v.Z() }; - return true; -} - -std::string formatGeometryEdgeLength(const GeometryRegistry& reg, const std::vector& ids) -{ - double total = 0.0; - std::ostringstream oss; - - for (Index id : ids) { - const TopoDS_Shape* s = reg.getEdge(id); - if (!s) { - oss << "边 " << id << ": 无效索引\n"; - continue; - } - GProp_GProps props; - BRepGProp::LinearProperties(*s, props); - const double len = props.Mass(); - total += len; - oss << "边 " << id << ": " << toString(len) << "\n"; - } - - oss << "累计长度: " << toString(total); - return oss.str(); -} - -std::string formatGeometryFaceArea(const GeometryRegistry& reg, const std::vector& ids) -{ - double total = 0.0; - std::ostringstream oss; - - for (Index id : ids) { - const TopoDS_Shape* s = reg.getFace(id); - if (!s) { - oss << "面 " << id << ": 无效索引\n"; - continue; - } - GProp_GProps props; - BRepGProp::SurfaceProperties(*s, props); - const double area = std::abs(props.Mass()); - total += area; - oss << "面 " << id << ": " << toString(area) << "\n"; - } - - oss << "总面积: " << toString(total); - return oss.str(); -} - -std::string formatGeometrySolidVolume(const GeometryRegistry& reg, const std::vector& ids) -{ - double total = 0.0; - std::ostringstream oss; - - for (Index id : ids) { - const TopoDS_Shape* s = reg.getSolid(id); - if (!s) { - oss << "体 " << id << ": 无效索引\n"; - continue; - } - GProp_GProps props; - BRepGProp::VolumeProperties(*s, props); - const double volume = std::abs(props.Mass()); - total += volume; - oss << "体 " << id << ": " << toString(volume) << "\n"; - } - - oss << "总体积: " << toString(total); - return oss.str(); -} - -std::string formatGeometryBoundingBox(const std::vector& shapes) -{ - Bnd_Box box; - for (const auto& s : shapes) - BRepBndLib::AddOptimal(s, box, false); // 不依赖三角剖分,直接按解析几何求界 - - if (box.IsVoid()) - return std::string("错误:无法计算包围盒"); - - Standard_Real xmin = 0.0, ymin = 0.0, zmin = 0.0, xmax = 0.0, ymax = 0.0, zmax = 0.0; - box.Get(xmin, ymin, zmin, xmax, ymax, zmax); - const Vec3 min { xmin, ymin, zmin }; - const Vec3 max { xmax, ymax, zmax }; - const Vec3 size = max - min; - - std::ostringstream oss; - oss << "包围盒:\n" - << "min: " << vecString(min) << "\n" - << "max: " << vecString(max) << "\n" - << "size: " << vecString(size); - return oss.str(); -} - -//! @brief 按边长 / 面积 / 体积加权的重心 -std::string formatGeometryCentroid(const GeometryRegistry& reg, - ElementEnum::Type type, const std::vector& ids) -{ - double total_mass = 0.0; - Vec3 sum { 0.0, 0.0, 0.0 }; - - for (Index id : ids) { - const TopoDS_Shape* s = findGeometryShape(reg, type, id); - if (!s) - continue; - - GProp_GProps props; - switch (type) { - case ElementEnum::GeometryEdge: - BRepGProp::LinearProperties(*s, props); - break; - case ElementEnum::GeometryFace: - BRepGProp::SurfaceProperties(*s, props); - break; - case ElementEnum::GeometrySolid: - BRepGProp::VolumeProperties(*s, props); - break; - default: - continue; - } - - const double mass = std::abs(props.Mass()); - if (mass < std::numeric_limits::epsilon()) - continue; - sum = sum + toVec3(props.CentreOfMass()) * mass; - total_mass += mass; - } - - if (total_mass < std::numeric_limits::epsilon()) - return std::string("错误:未找到可计算重心的几何对象"); - - std::ostringstream oss; - oss << "重心: " << vecString(sum / total_mass); - return oss.str(); -} - -// ---------------- 几何(OCC)测量实现:选择 id 为 GeometryRegistry 分配的全局几何 id ---------------- - -std::string geomDistance(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type != ElementEnum::GeometryVertex || selection.ids.size() != 2) { - return std::string("错误:距离测量需要选择两个几何点"); - } - Vec3 a, b; - if (!geometryVertexPoint(reg, selection.ids[0], a) || !geometryVertexPoint(reg, selection.ids[1], b)) - return std::string("错误:无效的几何点 id"); - return formatDistance(a, b); -} - -std::string geomAngle(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type == ElementEnum::GeometryVertex && selection.ids.size() == 3) { - Vec3 a, b, c; - if (!geometryVertexPoint(reg, selection.ids[0], a) - || !geometryVertexPoint(reg, selection.ids[1], b) - || !geometryVertexPoint(reg, selection.ids[2], c)) - return std::string("错误:无效的几何点 id"); - return formatAngle(a, b, c); - } - if (selection.type == ElementEnum::GeometryEdge && selection.ids.size() == 2) { - const TopoDS_Shape* e0 = reg.getEdge(selection.ids[0]); - const TopoDS_Shape* e1 = reg.getEdge(selection.ids[1]); - Vec3 u, v; - if (!e0 || !e1 - || !geometryEdgeDirection(TopoDS::Edge(*e0), u) - || !geometryEdgeDirection(TopoDS::Edge(*e1), v)) - return std::string("错误:无效的几何边 id 或边方向无法计算"); - std::ostringstream oss; - oss << "Angle: " << toString(angleBetween(u, v)) << " deg"; - return oss.str(); - } - return std::string("错误:角度测量需要选择三个几何点或两条几何边"); -} - -std::string geomRadius(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type == ElementEnum::GeometryVertex && selection.ids.size() == 3) { - Vec3 a, b, c; - if (!geometryVertexPoint(reg, selection.ids[0], a) - || !geometryVertexPoint(reg, selection.ids[1], b) - || !geometryVertexPoint(reg, selection.ids[2], c)) - return std::string("错误:无效的几何点 id"); - return formatRadius(a, b, c); - } - if (selection.type == ElementEnum::GeometryEdge && selection.ids.size() == 1) { - const TopoDS_Shape* s = reg.getEdge(selection.ids[0]); - if (!s) - return std::string("错误:无效的几何边 id"); - BRepAdaptor_Curve curve(TopoDS::Edge(*s)); - if (curve.GetType() != GeomAbs_Circle) - return std::string("错误:所选边不是圆弧,无法直接测半径(可改选三个几何点)"); - const gp_Circ circ = curve.Circle(); - std::ostringstream oss; - oss << "Radius: " << toString(circ.Radius()) << "\n" - << "Center: " << vecString(toVec3(circ.Location())); - return oss.str(); - } - return std::string("错误:半径测量需要选择三个几何点或一条圆弧边"); -} - -std::string geomLength(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type != ElementEnum::GeometryEdge || selection.ids.empty()) { - return std::string("错误:长度测量需要选择几何边"); - } - return formatGeometryEdgeLength(reg, selection.ids); -} - -std::string geomArea(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type != ElementEnum::GeometryFace || selection.ids.empty()) { - return std::string("错误:面积测量需要选择几何面"); - } - return formatGeometryFaceArea(reg, selection.ids); -} - -std::string geomVolume(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type != ElementEnum::GeometrySolid || selection.ids.empty()) { - return std::string("错误:体积测量需要选择几何体"); - } - return formatGeometrySolidVolume(reg, selection.ids); -} - -std::string geomBoundingBox(const GeometryRegistry& reg, const Selection& selection) -{ - const auto shapes = collectGeometryShapes(reg, selection.type, selection.ids); - if (shapes.empty()) { - return std::string("错误:未找到有效的几何对象"); - } - return formatGeometryBoundingBox(shapes); -} - -std::string geomCentroid(const GeometryRegistry& reg, const Selection& selection) -{ - if (selection.type == ElementEnum::GeometryVertex) { - std::vector points; - points.reserve(selection.ids.size()); - for (Index id : selection.ids) { - Vec3 p; - if (geometryVertexPoint(reg, id, p)) - points.push_back(p); - } - if (points.empty()) - return std::string("错误:未找到有效的几何点"); - return formatCentroid(points); - } - if (selection.ids.empty()) { - return std::string("错误:未找到有效的几何对象"); - } - return formatGeometryCentroid(reg, selection.type, selection.ids); -} - -// ---------------- 网格测量实现 ---------------- - -std::string meshDistance(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - if (selection.type != ElementEnum::Vertex || selection.ids.size() != 2) { - spdlog::error("MeasureHandler::execute: 距离测量需要选择两个点"); - return std::string("错误:距离测量需要选择两个点"); - } - const Vec3& a = getPosition(mesh, manager, selection.ids[0]); - const Vec3& b = getPosition(mesh, manager, selection.ids[1]); - return formatDistance(a, b); -} - -std::string meshAngle(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - if (selection.type == ElementEnum::Vertex && selection.ids.size() == 3) { - const Vec3& a = getPosition(mesh, manager, selection.ids[0]); - const Vec3& b = getPosition(mesh, manager, selection.ids[1]); - const Vec3& c = getPosition(mesh, manager, selection.ids[2]); - return formatAngle(a, b, c); - } - if (selection.type == ElementEnum::Edge && selection.ids.size() == 4) { - // 边选择的 ids 是每条边两个端点的顶点 id(EdgeSelectorHighlight 约定) - const auto& ids = selection.ids; - const bool valid = isValidVertexId(mesh, manager, ids[0]) && isValidVertexId(mesh, manager, ids[1]) - && isValidVertexId(mesh, manager, ids[2]) && isValidVertexId(mesh, manager, ids[3]); - if (valid) { - const Vec3 u = getPosition(mesh, manager, ids[1]) - getPosition(mesh, manager, ids[0]); - const Vec3 v = getPosition(mesh, manager, ids[3]) - getPosition(mesh, manager, ids[2]); - std::ostringstream oss; - oss << "Angle: " << toString(angleBetween(u, v)) << " deg"; - return oss.str(); - } - } - spdlog::error("MeasureHandler::execute: 角度测量需要选择三个点或两条边"); - return std::string("错误:角度测量需要选择三个点或两条边"); -} - -std::string meshRadius(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - if (selection.type != ElementEnum::Vertex || selection.ids.size() != 3) { - spdlog::error("MeasureHandler::execute: 半径测量需要选择三个点"); - return std::string("错误:半径测量需要选择三个点"); - } - const Vec3& a = getPosition(mesh, manager, selection.ids[0]); - const Vec3& b = getPosition(mesh, manager, selection.ids[1]); - const Vec3& c = getPosition(mesh, manager, selection.ids[2]); - return formatRadius(a, b, c); -} - -std::string meshLength(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - // 边选择的 ids 是每条边两个端点的顶点 id 对,数量应为正偶数 - if (selection.type != ElementEnum::Edge || selection.ids.empty() || selection.ids.size() % 2 != 0) { - spdlog::error("MeasureHandler::execute: 长度测量需要选择边"); - return std::string("错误:长度测量需要选择边"); - } - return formatEdgeLength(mesh, manager, selection.ids); -} - -std::string meshArea(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - if (selection.type != ElementEnum::Face) { - spdlog::error("MeasureHandler::execute: 面积测量需要选择面"); - return std::string("错误:面积测量需要选择面"); - } - return formatFaceArea(mesh, manager, selection.ids); -} - -std::string meshVolume(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - if (selection.type != ElementEnum::Solid) { - spdlog::error("MeasureHandler::execute: 体积测量需要选择体"); - return std::string("错误:体积测量需要选择体"); - } - return formatSolidVolume(mesh, manager, selection.ids); -} - -std::string meshBoundingBox(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - const auto positions = collectPositions(mesh, manager, selection.type, selection.ids); - if (positions.empty()) { - spdlog::error("MeasureHandler::execute: 未找到可用顶点"); - return std::string("错误:未找到可用顶点"); - } - return formatBoundingBox(positions); -} - -std::string meshCentroid(const MeshData& mesh, const ModelLayer& manager, const Selection& selection) -{ - const auto positions = collectPositions(mesh, manager, selection.type, selection.ids); - if (positions.empty()) { - spdlog::error("MeasureHandler::execute: 未找到可用顶点"); - return std::string("错误:未找到可用顶点"); - } - return formatCentroid(positions); -} - -//! @brief 测量操作注册表:新增测量类型只需在此加一行,args_type/校验/分发全部由表生成 -const MeasureOp kMeasureOps[] = { - { MeasureType::Distance, "距离", "Vertex", "GeometryVertex", meshDistance, geomDistance }, - { MeasureType::Angle, "角度", "Vertex,Edge", "GeometryVertex,GeometryEdge", meshAngle, geomAngle }, - { MeasureType::Radius, "半径", "Vertex", "GeometryVertex,GeometryEdge", meshRadius, geomRadius }, - { MeasureType::Length, "长度", "Edge", "GeometryEdge", meshLength, geomLength }, - { MeasureType::Area, "面积", "Face", "GeometryFace", meshArea, geomArea }, - { MeasureType::Volume, "体积", "Solid", "GeometrySolid", meshVolume, geomVolume }, - { MeasureType::BoundingBox, "包围盒", "Vertex,Edge,Face,Solid", "GeometryVertex,GeometryEdge,GeometryFace,GeometrySolid", meshBoundingBox, geomBoundingBox }, - { MeasureType::Centroid, "重心", "Vertex,Edge,Face,Solid", "GeometryVertex,GeometryEdge,GeometryFace,GeometrySolid", meshCentroid, geomCentroid }, -}; - -//! @brief 按下标查注册表,越界返回 nullptr -const MeasureOp* findMeasureOp(int index) -{ - if (index < 0 || index >= static_cast(sizeof(kMeasureOps) / sizeof(kMeasureOps[0]))) - return nullptr; - return &kMeasureOps[index]; -} - -//! @brief 由注册表生成 Combo 内容(名称与下标天然一致) -std::string comboContent() -{ - std::string out; - for (const MeasureOp& op : kMeasureOps) { - if (!out.empty()) - out += ","; - out += op.name; - } - return out; -} - -//! @brief 由注册表生成选择器内容:网格模式按 Vertex,Edge,Face,Solid 规范序取并集 -std::string selectorContent() -{ - static const char* canonical[] = { "Vertex", "Edge", "Face", "Solid" }; - std::string out; - for (const char* mode : canonical) { - bool used = false; - for (const MeasureOp& op : kMeasureOps) { - if (std::string(op.mesh_selector_modes).find(mode) != std::string::npos) { - used = true; - break; - } - } - if (used) { - if (!out.empty()) - out += ","; - out += mode; - } - } - return out; -} } void MeasureHandler::setup(FeatureRegistrar& reg) { - assertExecuteThread(); - // Combo 内容全部由测量操作注册表生成,名称与下标天然一致 - reg.addParameter({ ArgTypeEnum::Combo, "测量类型", comboContent() + "|0" }); - reg.addParameter({ ArgTypeEnum::Selector, "选择对象", selectorContent() }); reg.addMenuItem({ "工具", "测量" }); } -std::any MeasureHandler::execute(FeatureContext& ctx) -{ - assertExecuteThread(); - - const int* type_index = ctx.params.value(kTypeParam).get(); - if (!type_index) { - spdlog::error("MeasureHandler::execute: 测量类型参数无效"); - return std::string("错误:测量类型参数无效"); - } - - const auto selection_ptr = ctx.params.value(kSelectionParam).get(); - if (!selection_ptr || !*selection_ptr) { - spdlog::error("MeasureHandler::execute: 选择对象参数无效"); - return std::string("错误:选择对象参数无效"); - } - - const auto& selection = **selection_ptr; - - // 选择集未带组件 id 时回退当前活动组件 - Index selected_component_id = selection.component_id; - if (selected_component_id < 0) { - const auto active_component = ctx.activeComponent ? ctx.activeComponent() : std::nullopt; - if (active_component) { - selected_component_id = *active_component; - } - } - ComponentData* comp = ctx.model.findComponent(selected_component_id); - if (!comp) { - spdlog::error("MeasureHandler::execute: 找不到选择对象所在的组件"); - return std::string("错误:找不到选择对象所在的组件"); - } - ModelLayer& manager = ctx.model; - const MeasureOp* op = findMeasureOp(*type_index); - if (!op) { - spdlog::error("MeasureHandler::execute: 未知测量类型下标 {}", *type_index); - return std::string("错误:未知测量类型"); - } - - MeshData* mesh = comp->asMeshData(); - if (!mesh) { - // 无网格但有几何(STEP/IGES):走 OCC 几何测量 - if (comp->geometry) { - comp->geometry->ensureIndexBuilt(manager.geomRegistry()); - if (!op->geom_exec) - return std::string("错误:") + op->name + "测量暂不支持几何模型"; - return op->geom_exec(manager.geomRegistry(), selection); - } - spdlog::error("MeasureHandler::execute: 选择对象所在组件没有网格或几何数据"); - return std::string("错误:选择对象所在组件没有网格或几何数据"); - } - - return op->mesh_exec(*mesh, manager, selection); -} - void MeasureHandler::activate(FeatureContext& ctx) { - assertExecuteThread(); // 标注集绑定到交互状态:渲染层事件后从 InteractionState.annotations 拉取绘制 annotations_ = &ctx.interaction.annotations(); ctx.interaction.onActivate([this]() { this->clear(); }); @@ -976,23 +81,8 @@ void MeasureHandler::activate(FeatureContext& ctx) ctx.interaction.onPick([this](const PickInfo& p) { return this->onPick(p); }); ctx.interaction.onHover([this](const PickInfo& p) { return this->onHover(p); }); } -// ---------------- 交互回调(经 InteractionContext 注册) ---------------- - -namespace { -constexpr double kEps = 1e-9; -} -void MeasureHandler::assertExecuteThread() const -{ - assert(std::this_thread::get_id() == gui_thread_id_); -} - -void MeasureHandler::assertInteractiveThread() const -{ - if (interactive_thread_id_ == std::thread::id()) - interactive_thread_id_ = std::this_thread::get_id(); - assert(std::this_thread::get_id() == interactive_thread_id_); -} +// ---------------- 交互回调(经 InteractionContext 注册,渲染线程驱动) ---------------- bool MeasureHandler::samePoint(const PickInfo& a, const PickInfo& b) { @@ -1006,7 +96,6 @@ bool MeasureHandler::samePoint(const PickInfo& a, const PickInfo& b) bool MeasureHandler::onPick(const PickInfo& pick) { - assertInteractiveThread(); if (!pick.valid) return false; @@ -1025,7 +114,6 @@ bool MeasureHandler::onPick(const PickInfo& pick) bool MeasureHandler::onHover(const PickInfo& pick) { - assertInteractiveThread(); // 未吸附或无起笔:清除已有预览;本来无预览则无需刷新 if (!pending_ || !pick.valid) { if (!has_preview_) @@ -1145,7 +233,6 @@ void MeasureHandler::refreshAnnotations() void MeasureHandler::clear() { - assertInteractiveThread(); pending_.reset(); has_preview_ = false; lines_.clear(); diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.h b/plugins/feature/MeasurePlugin/MeasureHandler.h index b3265ff..caf81d9 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.h +++ b/plugins/feature/MeasurePlugin/MeasureHandler.h @@ -1,14 +1,13 @@ /** * @file MeasureHandler.h - * @brief 测量功能处理器声明:距离、角度、半径、长度、面积、体积、包围盒与重心 + * @brief 测量处理器声明:交互式两点成线测距与共端点夹角标注 * @author 范成通 email 1941804585@qq.com */ #pragma once #include "FeatureHandler.h" #include "InteractiveTypes.h" -#include +#include #include -#include #include namespace systems::feature { @@ -18,43 +17,29 @@ using systems::interaction::AnnotationLine; using systems::interaction::PickInfo; /** - * @brief 测量功能处理器:尺寸标注(FeatureHandler)+ 交互测量(经 InteractionContext 注册回调) + * @brief 测量处理器:交互式两点成线测距与共端点两线夹角标注 * - * 尺寸标注:功能参数(测量类型 + 选择对象)+ 菜单执行 → 返回结果文本(GUI 线程调用)。 - * 交互测量:两点成线、共端点两线自动标注夹角(渲染线程经 InteractionService 驱动)。 - * - * @par 线程模型与共享约束(修改前必读) - * - setup()/execute() 由 **GUI 线程**调用(FeatureSystem 注册与菜单触发)。 - * - onPick/onHover/annotations/clear 由 **渲染线程**调用 - * (InteractionService 经 dispatch_async 驱动;本类在 activate() 中经 ctx.interaction 注册回调)。 - * - 因此约定:**setup()/execute() 不得读写交互状态成员** - * (pending_、has_preview_、preview_、lines_、angles_); - * **交互事件方法不得触碰标注执行路径的状态**(execute 当前是无状态的,新增状态前必须评审)。 - * - 本类**无锁**。UI 层的模式互斥(交互模式隐藏执行按钮、参数模式停止交互)只是当前的安全网, - * 不是设计依据;确需跨两侧共享状态时,先加锁或经设计评审,不得直接访问。 - * 上述线程模型由 assertExecuteThread()/assertInteractiveThread() 在调试期以 assert 强制。 + * 纯视口交互功能:无参数、无执行路径;交互回调(拾取/悬停/激活/停用/清除) + * 由渲染线程经 InteractionService 驱动,全部状态成员仅被交互回调访问。 */ class MeasureHandler : public FeatureHandler { public: MeasureHandler() = default; ~MeasureHandler() override = default; - //! @brief 声明功能参数(测量类型、选择对象)与菜单项 + //! @brief 声明菜单项 void setup(FeatureRegistrar& reg) override; //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用/清除) void activate(FeatureContext& ctx) override; - //! @brief 尺寸标注:按参数中的测量类型与选择对象执行,返回结果文本 - std::any execute(FeatureContext& ctx) override; //! @brief 交互测量状态查询(测试与面板用) int lineCount() const { return static_cast(lines_.size()); } bool hasPending() const { return pending_.has_value(); } private: - // ---- 交互回调(原 InteractiveHandler 接口方法,现由 activate() 注册到 InteractionContext) ---- + // ---- 交互回调(由 activate() 注册到 InteractionContext,渲染线程驱动) ---- bool onPick(const PickInfo& pick); bool onHover(const PickInfo& pick); - const AnnotationBatch& annotations() const { assertInteractiveThread(); return *annotations_; } void clear(); //! @brief 交互测量线:两个吸附点(PickInfo 已含世界坐标与两套顶点 id) @@ -76,23 +61,13 @@ private: //! @brief 由当前状态重建标注集 void refreshAnnotations(); - //! @brief 线程亲和守卫(仅调试期生效):setup/execute 必须在构造线程(GUI)调用 - void assertExecuteThread() const; - //! @brief 线程亲和守卫(仅调试期生效):交互方法必须始终在单一线程上调用 - //! (应用内为渲染线程;测试单线程环境下与构造线程相同也合法) - void assertInteractiveThread() const; - std::optional pending_; //> 已起笔未成线的首点 bool has_preview_ = false; //> 悬停是否正在预览 PickInfo preview_ {}; //> 悬停预览吸附点 - // 以下交互状态成员仅允许渲染线程的交互方法访问(见类注释的线程模型约定) std::vector lines_; std::vector angles_; // 标注集契约:功能在回调中直接更新 InteractionState.annotations(activate 时绑定), // 渲染层事件后拉取绘制;自持成员会导致渲染层永远拉到空标注 AnnotationBatch* annotations_ { nullptr }; - - const std::thread::id gui_thread_id_ = std::this_thread::get_id(); //> 构造线程(GUI 线程)id - mutable std::thread::id interactive_thread_id_ {}; //> 首个交互调用所在线程 id }; } diff --git a/plugins/feature/MeasurePlugin/MeasurePlugin.json b/plugins/feature/MeasurePlugin/MeasurePlugin.json index e9f009f..18733c5 100644 --- a/plugins/feature/MeasurePlugin/MeasurePlugin.json +++ b/plugins/feature/MeasurePlugin/MeasurePlugin.json @@ -3,9 +3,7 @@ "handler": { "name": "MeasurePlugin", "display_name": "测量", - "description": "交互式测量与尺寸标注:两点成线、共点夹角、距离/角度/半径/长度/面积/体积/包围盒/重心", - "interactive": true, - "execute_text": "尺寸标注", - "interaction_guide": "依次点击拾取两点构成一条直线并显示长度;可连续画多条线,两线共端点时自动显示夹角;同一点连点两次取消当前起笔。" + "description": "交互式测量:依次点击拾取两点构成一条直线并显示长度;可连续画多条线,两线共端点时自动显示夹角;同一点连点两次取消当前起笔。", + "interactive": true } } diff --git a/plugins/feature/MeasurePlugin/test/CMakeLists.txt b/plugins/feature/MeasurePlugin/test/CMakeLists.txt index 595471c..f4ea5bf 100644 --- a/plugins/feature/MeasurePlugin/test/CMakeLists.txt +++ b/plugins/feature/MeasurePlugin/test/CMakeLists.txt @@ -1,2 +1,2 @@ precess_add_test(TestMeasureHandler TestMeasureHandler.cpp) -precess_test_link_libraries(TestMeasureHandler MeasurePluginlib DataTest TKPrim TKBRep TKTopAlgo TKernel) +precess_test_link_libraries(TestMeasureHandler MeasurePluginlib) diff --git a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp index 99da86e..5c2fc36 100644 --- a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp @@ -1,54 +1,28 @@ /** * @file TestMeasureHandler.cpp - * @brief 测量功能处理器的单元测试 + * @brief 测量处理器的单元测试 * @author 范成通 email 1941804585@qq.com */ #include "MeasureHandler.h" -#include "ArgObject.h" -#include "ComponentData.h" #include "EventBus.h" #include "FeatureContext.h" #include "FeatureParams.h" #include "FeatureRegistrar.h" -#include "GeometryData.h" #include "InteractionContext.h" #include "InteractionState.h" -#include "MakeMeshData.h" -#include "MeshData.h" #include "ModelLayer.h" -#include "Selection.h" - -#include -#include #include -#include #include #include -#include using namespace systems::feature; namespace { -std::shared_ptr makeVertexSelection(const std::vector& ids) -{ - return std::make_shared(Selection { ids, ElementEnum::Vertex, 0 }); -} - -std::shared_ptr makeEdgeSelection(const std::vector& ids) -{ - return std::make_shared(Selection { ids, ElementEnum::Edge, 0 }); -} - -std::shared_ptr makeGeometrySelection(ElementEnum::Type type, const std::vector& ids) -{ - return std::make_shared(Selection { ids, type, 0 }); -} - -//! @brief 功能测试环境:手工装配 FeatureContext(参数集 + 活动组件 provider),替代原算法 HandlerContext +//! @brief 功能测试环境:手工装配 FeatureContext(参数集 + 交互上下文),替代运行时注册流程 struct FeatureTestEnv { ModelLayer mgr; core::EventBus bus; @@ -76,80 +50,18 @@ struct FeatureTestEnv { [this](Index component_id) { return mgr.getComponentOperator(component_id); }, }); } - - //! @brief 写入功能参数并执行尺寸标注,返回结果字符串(下标 0=测量类型 1=选择对象,见 setup) - std::string executeMeasure(int measure_type_index, std::shared_ptr selection) - { - params->setValue(0, core::ArgObject::create(measure_type_index)); - params->setValue(1, core::ArgObject::create(std::move(selection))); - auto result_any = handler.execute(*ctx); - if (const std::string* result = std::any_cast(&result_any)) - return *result; - return {}; - } }; -//! @brief 在 10×10×10 的 OCC 立方体几何组件上执行一次测量,返回结果字符串 -std::string executeGeometryMeasureOnBox(int measure_type_index, ElementEnum::Type sel_type, int local_id) -{ - BRepPrimAPI_MakeBox box_maker(10.0, 10.0, 10.0); - box_maker.Build(); - if (!box_maker.IsDone()) - return {}; - - auto geometry = std::make_unique(); - geometry->rootShape = std::make_unique(box_maker.Shape()); - - auto c = std::make_unique(); - c->id = -1; - c->name = "box"; - c->geometry = std::move(geometry); - - ComponentDatas comps; - comps.push_back(std::move(c)); - - FeatureTestEnv env; - const Index model_id = env.mgr.addModel("box", std::move(comps)); - auto cids = env.mgr.modelById(model_id)->componentIds(); - if (cids.size() != 1) - return {}; - - ComponentData* comp = env.mgr.findComponent(cids[0]); - if (!comp) - return {}; - comp->geometry->ensureIndexBuilt(env.mgr.geomRegistry()); - - Index gid = -1; - switch (sel_type) { - case ElementEnum::GeometryEdge: - gid = comp->geometry->index.edgeGlobalId(local_id); - break; - case ElementEnum::GeometryFace: - gid = comp->geometry->index.faceGlobalId(local_id); - break; - case ElementEnum::GeometrySolid: - gid = comp->geometry->index.solidGlobalId(local_id); - break; - default: - break; - } - - auto selection = makeGeometrySelection(sel_type, { gid }); - selection->component_id = cids[0]; - return env.executeMeasure(measure_type_index, selection); -} - } // namespace -TEST_CASE("MeasureHandler setup declares measure parameters and menu") +TEST_CASE("MeasureHandler setup declares menu only") { MeasureHandler handler; FeatureRegistrar reg; handler.setup(reg); - REQUIRE(reg.argTypes().size() == 2); - CHECK(reg.argTypes()[0].type == ArgTypeEnum::Combo); - CHECK(reg.argTypes()[1].type == ArgTypeEnum::Selector); + // 纯交互功能:无参数,仅菜单项 + CHECK(reg.argTypes().empty()); REQUIRE(reg.menuItems().size() == 1); } @@ -188,117 +100,3 @@ TEST_CASE("MeasureHandler: interactive picks update state annotations and onClea CHECK(env.interaction_state_.annotations.points.empty()); CHECK(env.interaction_state_.annotations.texts.empty()); } - -TEST_CASE("MeasureHandler: distance between two vertices") -{ - auto mesh = std::make_unique(MakeMeshData()); - auto c = std::make_unique(); - c->id = -1; - c->name = "Comp"; - c->mesh = std::move(mesh); - - ComponentDatas comps; - comps.push_back(std::move(c)); - - FeatureTestEnv env; - const Index model_id = env.mgr.addModel("test", std::move(comps)); - - auto cids = env.mgr.modelById(model_id)->componentIds(); - REQUIRE(cids.size() == 1); - - ComponentData* comp = env.mgr.findComponent(cids[0]); - REQUIRE(comp); - - MeshData* mesh_ptr = comp->asMeshData(); - REQUIRE(mesh_ptr); - const Index base = mesh_ptr->local_to_global_[0]; - - auto selection = makeVertexSelection({ base + 0, base + 1 }); - selection->component_id = cids[0]; - const std::string result = env.executeMeasure(0, selection); // 距离 - CHECK(result.find("Distance") != std::string::npos); - CHECK(result.find("1.000000") != std::string::npos); -} - -TEST_CASE("MeasureHandler: length of edges selected as endpoint vertex id pairs") -{ - auto mesh = std::make_unique(MakeMeshData()); - auto c = std::make_unique(); - c->id = -1; - c->name = "Comp"; - c->mesh = std::move(mesh); - - ComponentDatas comps; - comps.push_back(std::move(c)); - - FeatureTestEnv env; - const Index model_id = env.mgr.addModel("test", std::move(comps)); - - auto cids = env.mgr.modelById(model_id)->componentIds(); - REQUIRE(cids.size() == 1); - - ComponentData* comp = env.mgr.findComponent(cids[0]); - REQUIRE(comp); - - MeshData* mesh_ptr = comp->asMeshData(); - REQUIRE(mesh_ptr); - const Index base = mesh_ptr->local_to_global_[0]; - - // 边选择的 ids 是端点顶点 id 对:{12,13} 与 {0,1} 长度均为 1 - auto selection = makeEdgeSelection({ base + 12, base + 13, base + 0, base + 1 }); - selection->component_id = cids[0]; - const std::string result = env.executeMeasure(3, selection); // 长度 - CHECK(result.find("累计长度: 2.000000") != std::string::npos); -} - -TEST_CASE("MeasureHandler: angle between two edges selected as endpoint vertex id pairs") -{ - auto mesh = std::make_unique(MakeMeshData()); - auto c = std::make_unique(); - c->id = -1; - c->name = "Comp"; - c->mesh = std::move(mesh); - - ComponentDatas comps; - comps.push_back(std::move(c)); - - FeatureTestEnv env; - const Index model_id = env.mgr.addModel("test", std::move(comps)); - - auto cids = env.mgr.modelById(model_id)->componentIds(); - REQUIRE(cids.size() == 1); - - ComponentData* comp = env.mgr.findComponent(cids[0]); - REQUIRE(comp); - - MeshData* mesh_ptr = comp->asMeshData(); - REQUIRE(mesh_ptr); - const Index base = mesh_ptr->local_to_global_[0]; - - // 边 {0,1} 方向 (1,0,0),边 {0,3} 方向 (0,1,0),夹角 90° - auto selection = makeEdgeSelection({ base + 0, base + 1, base + 0, base + 3 }); - selection->component_id = cids[0]; - const std::string result = env.executeMeasure(1, selection); // 角度 - CHECK(result.find("Angle: 90.000000 deg") != std::string::npos); -} - -TEST_CASE("MeasureHandler: geometry edge length on OCC box") -{ - // 10×10×10 立方体任一条几何边长度均为 10 - const std::string result = executeGeometryMeasureOnBox(3, ElementEnum::GeometryEdge, 1); - CHECK(result.find("累计长度: 10.000000") != std::string::npos); -} - -TEST_CASE("MeasureHandler: geometry face area on OCC box") -{ - // 10×10×10 立方体任一个几何面面积均为 100 - const std::string result = executeGeometryMeasureOnBox(4, ElementEnum::GeometryFace, 1); - CHECK(result.find("总面积: 100.000000") != std::string::npos); -} - -TEST_CASE("MeasureHandler: geometry solid volume on OCC box") -{ - // 10×10×10 立方体体积为 1000 - const std::string result = executeGeometryMeasureOnBox(5, ElementEnum::GeometrySolid, 1); - CHECK(result.find("总体积: 1000.000000") != std::string::npos); -} -- Gitee From 5d89eefa51994d5fea2ce862f56ea0ea0a3fc30e Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sat, 25 Jul 2026 22:51:34 +0800 Subject: [PATCH 05/13] =?UTF-8?q?fix(app):=20=E4=BF=AE=E5=A4=8D=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E4=BA=A4=E4=BA=92=E5=8A=9F=E8=83=BD=E5=90=8E=E8=A7=86?= =?UTF-8?q?=E5=8F=A3=E6=97=A0=E6=B3=95=E6=8B=BE=E5=8F=96=E7=82=B9=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 onActiveOpChanged 内读取 hasInteractive 绑定做激活判定,但属性变更处理器与依赖同一属性的其它绑定之间求值次序无保证:从空操作/非交互操作切换到交互功能时可能读到旧值 false,导致 setInteractionActive 收到空串、交互从未激活,左键全部落空到选择系统(旧双模式实现靠 onHasInteractiveChanged 冗余触发器补偿该时序,精简时被移除)。 改为在处理器内直接读取 activeOp 判定(触发本次变更的属性值保证为最新),从根源上消除对绑定求值次序的依赖。 --- app/SideBar.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index 3a3fd3e..ab285e7 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -24,7 +24,10 @@ Item{ parameters = [] resultText = "" // 活动操作声明视口交互能力则激活其交互,否则全部下线(幂等,守卫在功能系统内) - QModelManager.featureSystem.setInteractionActive(hasInteractive ? activeOp.info.name : "") + // 必须直接读 activeOp 判定:本处理器与 hasInteractive 绑定由同一变更驱动, + // 求值次序无保证,读绑定可能拿到旧值导致激活被跳过 + var interactive = !!(activeOp && activeOp.info && activeOp.info.interactive) + QModelManager.featureSystem.setInteractionActive(interactive ? activeOp.info.name : "") } // 写入参数值;功能的参数为持久参数,修改即时写回功能系统实时生效 -- Gitee From ce90c39a65b888129ab1226e3382474361eb8ee8 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sun, 26 Jul 2026 00:16:09 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat(core,systems,app,plugins):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Button=20=E5=8F=82=E6=95=B0=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E4=B8=8E=20on=5Faction=20=E8=A7=86=E5=8F=A3=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core 新增 ArgTypeEnum::Button:无值触发器,载荷为点击计数(功能约定忽略值、只读参数下标) - FeatureParams 支持 Button 默认值;InteractionState 新增 on_action 动作槽,InteractionContext 提供 onAction 订阅(渲染线程驱动) - app/render 新增 InteractionService::postAction 与 QRenderWindow::postInteractionAction,面板按钮点击经 dispatch_async 于渲染线程回到功能 - SideBar 新增通用按钮参数控件:交互功能走渲染线程动作投递,普通功能走参数事件广播 - MeasurePlugin 注册"清除"按钮参数并订阅 on_action 清理会话;同步补充 TestInteractionService / TestMeasureHandler 用例 --- app/SideBar.qml | 24 +++++++++++++++++++ app/render/InteractionService.cpp | 9 +++++++ app/render/InteractionService.h | 5 +++- app/render/QRenderWindow.cpp | 7 ++++++ app/render/QRenderWindow.h | 8 ++++++- app/render/test/TestInteractionService.cpp | 12 +++++++++- core/ArgType.h | 9 ++++++- core/private_ArgTypeEnum.h | 3 ++- model/systems/core/InteractionState.h | 4 +++- model/systems/feature/FeatureParams.cpp | 3 +++ model/systems/feature/InteractionContext.cpp | 5 ++++ model/systems/feature/InteractionContext.h | 4 +++- .../feature/MeasurePlugin/MeasureHandler.cpp | 3 +++ .../feature/MeasurePlugin/MeasureHandler.h | 4 ++-- .../MeasurePlugin/test/TestMeasureHandler.cpp | 21 ++++++++++++---- 15 files changed, 108 insertions(+), 13 deletions(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index ab285e7..5bf0218 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -131,6 +131,9 @@ Item{ if(model.type === QArgType.Bool){ //布尔值 return boolComponent } + if(model.type === QArgType.Button){ //按钮 + return buttonComponent + } } } } @@ -418,4 +421,25 @@ Item{ } } } + Component{ + id: buttonComponent + RowLayout{ + spacing: 5 + width: parameterList.width + Button{ + // Button 是无值触发器:交互功能的动作经渲染线程投递(on_action,线程模型约定); + // 普通功能走参数事件广播(计数器载荷,功能约定忽略值只读参数下标) + text: model.name + Layout.fillWidth: true + onClicked:{ + if (root.hasInteractive) { + if (App.registry.renderWindow) + App.registry.renderWindow.postInteractionAction(index) + } else { + root.setParam(index, (root.parameters[index] || 0) + 1) + } + } + } + } + } } diff --git a/app/render/InteractionService.cpp b/app/render/InteractionService.cpp index 3c5bab4..3a3e35a 100644 --- a/app/render/InteractionService.cpp +++ b/app/render/InteractionService.cpp @@ -206,6 +206,15 @@ void InteractionService::clear() refreshAnnotations(); } +void InteractionService::postAction(int param_index) +{ + if (!current_) + return; + if (current_->on_action) + current_->on_action(param_index); + refreshAnnotations(); +} + void InteractionService::clearActors() { points_poly_->Initialize(); diff --git a/app/render/InteractionService.h b/app/render/InteractionService.h index 23124eb..4d6d29a 100644 --- a/app/render/InteractionService.h +++ b/app/render/InteractionService.h @@ -48,8 +48,11 @@ public: void pick(double posx, double posy); //! @brief 悬停:同步激活状态,调用 on_hover 回调做动态预览 void hover(double posx, double posy); - //! @brief 面板"清除":调用当前状态的 on_clear 回调并刷新标注 + //! @brief 面板"确认"按钮:调用当前状态的 on_clear 回调并刷新标注 void clear(); + //! @brief 面板动作按钮(Button 参数):调用当前状态的 on_action 回调并刷新标注 + //! @param param_index 按钮参数下标,作为动作 id 透传给功能 + void postAction(int param_index); private: //! @brief 同步激活状态:迁移时执行下线(on_deactivate/清标注/还原吸附)与上线(吸附/on_activate/刷新) systems::interaction::InteractionState* syncState(); diff --git a/app/render/QRenderWindow.cpp b/app/render/QRenderWindow.cpp index 87b882b..596298d 100644 --- a/app/render/QRenderWindow.cpp +++ b/app/render/QRenderWindow.cpp @@ -562,6 +562,13 @@ void QRenderWindow::clearInteraction() }); } +void QRenderWindow::postInteractionAction(int param_index) +{ + dispatch_async([this, param_index](vtkRenderWindow* renderWindow, vtkUserData userData) -> void { + interaction_service_->postAction(param_index); + }); +} + void QRenderWindow::setClick() { dispatch_async([this](vtkRenderWindow* renderWindow, vtkUserData userData) { diff --git a/app/render/QRenderWindow.h b/app/render/QRenderWindow.h index c1eef6f..816a236 100644 --- a/app/render/QRenderWindow.h +++ b/app/render/QRenderWindow.h @@ -112,10 +112,16 @@ public: Q_INVOKABLE void setFeatureAdaptor(QObject* adaptor); /** - * @brief 清空当前交互状态与视图内标注(面板"清除"按钮) + * @brief 清空当前交互状态与视图内标注(面板"确认"按钮的会话清理) */ Q_INVOKABLE void clearInteraction(); + /** + * @brief 向当前激活交互投递面板动作按钮事件(Button 参数,渲染线程执行) + * @param param_index 按钮参数下标,作为动作 id 透传给功能 + */ + Q_INVOKABLE void postInteractionAction(int param_index); + /** * @brief 显示/隐藏比例尺(测量插件激活时启用,随相机缩放自动更新刻度) * @param on 是否显示 diff --git a/app/render/test/TestInteractionService.cpp b/app/render/test/TestInteractionService.cpp index 1c582b8..07344ac 100644 --- a/app/render/test/TestInteractionService.cpp +++ b/app/render/test/TestInteractionService.cpp @@ -50,6 +50,8 @@ struct FakeInteraction { int activate_count = 0; int deactivate_count = 0; int clear_count = 0; + int action_count = 0; + int last_action = -1; int hover_count = 0; std::vector picks; @@ -58,6 +60,10 @@ struct FakeInteraction { state.on_activate = [this] { ++activate_count; }; state.on_deactivate = [this] { ++deactivate_count; }; state.on_clear = [this] { ++clear_count; }; + state.on_action = [this](int param_index) { + ++action_count; + last_action = param_index; + }; state.on_pick = [this](const PickInfo& pick) { picks.push_back(pick); return true; @@ -212,10 +218,14 @@ int main(int argc, char* argv[]) service.pick(5000, 5000); // 窗口外坐标,拾取器必然未命中 check(fake.picks.size() == picks_before, "未命中吸附点时不调用 onPick"); - // ---- clear:面板"清除"转发到处理器 ---- + // ---- clear:面板"确认"的会话清理转发到处理器 ---- service.clear(); check(fake.clear_count == 1, "clear() 转发到处理器"); + // ---- postAction:面板动作按钮转发到处理器并携带参数下标 ---- + service.postAction(2); + check(fake.action_count == 1 && fake.last_action == 2, "postAction() 转发到处理器并携带参数下标"); + // ---- 几何(OCC)顶点拾取:geom_id 填充 ---- { TopoDS_Shape box = BRepPrimAPI_MakeBox(gp_Pnt(500.0, 500.0, 0.0), 1000.0, 800.0, 600.0).Shape(); diff --git a/core/ArgType.h b/core/ArgType.h index 615a92f..556b01a 100644 --- a/core/ArgType.h +++ b/core/ArgType.h @@ -77,7 +77,14 @@ struct ArgType::TypeMap { template <> struct ArgType::TypeMap { using type = std::shared_ptr; -}; +}; +/** + * @brief 按钮类型,内容为点击计数器(无值触发器,仅作事件触发载荷,功能约定忽略值只读参数下标) + */ +template <> +struct ArgType::TypeMap { + using type = int; +}; } #endif // !ARG_TYPE_H \ No newline at end of file diff --git a/core/private_ArgTypeEnum.h b/core/private_ArgTypeEnum.h index 567bf7f..80dbee9 100644 --- a/core/private_ArgTypeEnum.h +++ b/core/private_ArgTypeEnum.h @@ -16,5 +16,6 @@ enum class ArgTypeEnum { // OBJECT, // 对象类型 // ARRAY, // 数组类型 // MAP, // 映射类型 - Selector // 选择器类型 + Selector, // 选择器类型 + Button // 按钮类型(无值触发器:点击即动作,语义由功能自定义;载荷为点击计数器,功能约定忽略值只读下标) }; \ No newline at end of file diff --git a/model/systems/core/InteractionState.h b/model/systems/core/InteractionState.h index b8dfce9..30406a5 100644 --- a/model/systems/core/InteractionState.h +++ b/model/systems/core/InteractionState.h @@ -23,7 +23,9 @@ struct InteractionState { bool active = false; //> 交互激活态:功能经 setActive 写入(GUI 线程),渲染层读取路由拾取 std::function on_activate; //> 交互会话开始(通常清空功能内部状态) std::function on_deactivate; //> 交互会话结束 - std::function on_clear; //> 面板"清除"按钮:清空当前会话状态 + std::function on_clear; //> 面板"确认"按钮的会话清理:清空当前会话状态 + //! @brief 面板动作按钮(Button 参数):参数下标为动作 id,动作语义由功能自定义 + std::function on_action; //! @brief 左键拾取:返回是否有状态变化(需要刷新标注) std::function on_pick; //! @brief 悬停:返回是否更新了预览(需要刷新标注) diff --git a/model/systems/feature/FeatureParams.cpp b/model/systems/feature/FeatureParams.cpp index 12b53ed..3cf5b0b 100644 --- a/model/systems/feature/FeatureParams.cpp +++ b/model/systems/feature/FeatureParams.cpp @@ -60,6 +60,9 @@ namespace { return core::ArgObject::create(parseComboIndex(type.content)); case ArgTypeEnum::Selector: return core::ArgObject::create({}); + case ArgTypeEnum::Button: + // Button 为无值触发器:默认 0 次点击,载荷仅作触发事件的计数器 + return core::ArgObject::create(0); case ArgTypeEnum::None: default: spdlog::warn("FeatureParams: param '{}' has None type, storing empty value", type.name); diff --git a/model/systems/feature/InteractionContext.cpp b/model/systems/feature/InteractionContext.cpp index 39c4fd3..27a196c 100644 --- a/model/systems/feature/InteractionContext.cpp +++ b/model/systems/feature/InteractionContext.cpp @@ -26,6 +26,11 @@ void InteractionContext::onClear(std::function cb) state_->on_clear = std::move(cb); } +void InteractionContext::onAction(std::function cb) +{ + state_->on_action = std::move(cb); +} + void InteractionContext::onPick(std::function cb) { state_->on_pick = std::move(cb); diff --git a/model/systems/feature/InteractionContext.h b/model/systems/feature/InteractionContext.h index 721f637..2eb1614 100644 --- a/model/systems/feature/InteractionContext.h +++ b/model/systems/feature/InteractionContext.h @@ -26,8 +26,10 @@ public: void onActivate(std::function cb); //! @brief 订阅交互会话结束(渲染线程) void onDeactivate(std::function cb); - //! @brief 订阅"清除"(面板清除按钮,渲染线程) + //! @brief 订阅会话清理(面板"确认"按钮,渲染线程) void onClear(std::function cb); + //! @brief 订阅面板动作按钮(Button 参数,渲染线程;参数下标为动作 id,语义由功能自定义) + void onAction(std::function cb); //! @brief 订阅左键拾取(渲染线程;返回是否有状态变化需要刷新标注) void onPick(std::function cb); //! @brief 订阅悬停(渲染线程;返回是否更新预览需要刷新标注) diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.cpp b/plugins/feature/MeasurePlugin/MeasureHandler.cpp index 4a746fc..7d6e702 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/MeasureHandler.cpp @@ -68,6 +68,8 @@ double angleBetween(const Vec3& u, const Vec3& v) void MeasureHandler::setup(FeatureRegistrar& reg) { + // "清除"按钮参数:无值触发器,点击经 on_action 于渲染线程回到本功能 + reg.addParameter({ ArgTypeEnum::Button, "清除", "" }); reg.addMenuItem({ "工具", "测量" }); } @@ -78,6 +80,7 @@ void MeasureHandler::activate(FeatureContext& ctx) ctx.interaction.onActivate([this]() { this->clear(); }); ctx.interaction.onDeactivate([this]() { this->clear(); }); ctx.interaction.onClear([this]() { this->clear(); }); + ctx.interaction.onAction([this](int) { this->clear(); }); ctx.interaction.onPick([this](const PickInfo& p) { return this->onPick(p); }); ctx.interaction.onHover([this](const PickInfo& p) { return this->onHover(p); }); } diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.h b/plugins/feature/MeasurePlugin/MeasureHandler.h index caf81d9..7788658 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.h +++ b/plugins/feature/MeasurePlugin/MeasureHandler.h @@ -27,9 +27,9 @@ public: MeasureHandler() = default; ~MeasureHandler() override = default; - //! @brief 声明菜单项 + //! @brief 声明"清除"按钮参数与菜单项 void setup(FeatureRegistrar& reg) override; - //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用/清除) + //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用/会话清理/动作按钮) void activate(FeatureContext& ctx) override; //! @brief 交互测量状态查询(测试与面板用) diff --git a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp index 5c2fc36..04c03d5 100644 --- a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp @@ -54,14 +54,15 @@ struct FeatureTestEnv { } // namespace -TEST_CASE("MeasureHandler setup declares menu only") +TEST_CASE("MeasureHandler setup declares clear button parameter and menu") { MeasureHandler handler; FeatureRegistrar reg; handler.setup(reg); - // 纯交互功能:无参数,仅菜单项 - CHECK(reg.argTypes().empty()); + // 纯交互功能:仅"清除"按钮参数(无值触发器)与菜单项 + REQUIRE(reg.argTypes().size() == 1); + CHECK(reg.argTypes()[0].type == ArgTypeEnum::Button); REQUIRE(reg.menuItems().size() == 1); } @@ -91,7 +92,7 @@ TEST_CASE("MeasureHandler: interactive picks update state annotations and onClea CHECK(env.interaction_state_.annotations.points.size() == 2); CHECK(env.interaction_state_.annotations.texts.size() == 1); - // 面板"清除":on_clear 清空会话状态与标注 + // 面板"确认"的会话清理:on_clear 清空会话状态与标注 REQUIRE(env.interaction_state_.on_clear); env.interaction_state_.on_clear(); CHECK(env.handler.lineCount() == 0); @@ -99,4 +100,16 @@ TEST_CASE("MeasureHandler: interactive picks update state annotations and onClea CHECK(env.interaction_state_.annotations.lines.empty()); CHECK(env.interaction_state_.annotations.points.empty()); CHECK(env.interaction_state_.annotations.texts.empty()); + + // 面板动作按钮("清除"参数):on_action 同样清空会话状态与标注 + env.interaction_state_.on_pick(p1); + env.interaction_state_.on_pick(p2); + CHECK(env.handler.lineCount() == 1); + REQUIRE(env.interaction_state_.on_action); + env.interaction_state_.on_action(0); + CHECK(env.handler.lineCount() == 0); + CHECK(!env.handler.hasPending()); + CHECK(env.interaction_state_.annotations.lines.empty()); + CHECK(env.interaction_state_.annotations.points.empty()); + CHECK(env.interaction_state_.annotations.texts.empty()); } -- Gitee From ca2cd8948d6db252b9938f526df03db06ddba102 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sun, 26 Jul 2026 00:16:22 +0800 Subject: [PATCH 07/13] =?UTF-8?q?feat(app):=20SideBar=20"=E6=B8=85?= =?UTF-8?q?=E9=99=A4"=E6=8C=89=E9=92=AE=E6=94=B9=E4=B8=BA"=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4"=EF=BC=8C=E7=BB=93=E6=9D=9F=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 确认 = 结束当前操作:交互功能先经 clearInteraction 及时清理交互系统(标注立即消失),再取消操作选中(停用交互) - 按钮对所有操作可用(不再限交互功能可见);再次执行需重新点选算法 - 启停为幂等状态应用,重复触发无副作用 --- app/SideBar.qml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index 5bf0218..34ca0ce 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -66,13 +66,16 @@ Item{ } } Button{ - id: clearButton - text: "清除" - visible: root.hasInteractive + id: confirmButton + text: "确认" + enabled: !!(root.activeOp && root.activeOp.info) Layout.fillWidth: true + // 确认 = 结束当前操作:交互功能先及时清理交互系统(标注立即消失), + // 再取消操作选中(停用交互);再次执行需重新点选算法 onClicked:{ - if (App.registry.renderWindow) + if (root.hasInteractive && App.registry.renderWindow) App.registry.renderWindow.clearInteraction() + App.activeOperation = null } } } -- Gitee From 1682f7e3e6c7fc474ad886d1fa281671887dfe16 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sun, 26 Jul 2026 00:18:34 +0800 Subject: [PATCH 08/13] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=85=B1=E4=BA=AB=E5=A4=B4=E6=96=87=E4=BB=B6=E9=9C=80?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E6=9E=84=E5=BB=BA=E7=9A=84=E6=B3=A8=E6=84=8F?= =?UTF-8?q?=E4=BA=8B=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次"测量崩溃"的根因是:修改被插件共享的 core/model 头文件 (InteractionState.h 新增 on_action 成员)后只构建 PreCess 主目标, 陈旧的 MeasurePlugin.dll 与新的 InteractionState 布局 ABI 错位, 导致 heap 内存错乱。插件 DLL 运行时动态加载、不是 PreCess.exe 的 链接依赖,单独构建主目标不会让插件随之重建。在 AGENTS.md 第 8 节 补充此注意事项,避免再次踩坑。 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index a5ce5b6..8ff1653 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,6 +135,7 @@ - **新特性必须配套测试用例**;修 bug 时尽量补可复现的回归测试。 - 不要向无测试的模块强行塞测试框架;遵循该模块既有测试模式。 - 工具检测用 PowerShell:例如 `Get-Command makensis`(不要用 `where makensis`)。 +- **插件共享头文件需全量构建**:修改被插件共享的 `core/`、`model/` 头文件(如 `InteractionState.h`、`InteractiveTypes.h`)后必须全量构建(含插件目标)再做手动验证:插件 DLL 运行时动态加载、不是 `PreCess.exe` 的链接依赖,`cmake --build --target PreCess` 不会让插件随之重建;新旧 ABI 混用会产生难以排查的内存错乱(如"测量崩溃"即此原因)。 --- -- Gitee From bd9629dabb4cdaf447637c2362cf2f92b3f64ee7 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sun, 26 Jul 2026 10:25:08 +0800 Subject: [PATCH 09/13] =?UTF-8?q?refactor(app):=20SideBar=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=BA=A4=E4=BA=92=E8=83=BD=E5=8A=9B=E7=89=B9=E5=88=A4?= =?UTF-8?q?=EF=BC=8C=E6=89=A7=E8=A1=8C/=E7=A1=AE=E8=AE=A4=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=BA=E9=80=9A=E7=94=A8=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 hasInteractive 属性及所有引用(commitButton visible 守卫、 confirmButton clearInteraction 分支、buttonComponent postInteractionAction 分支) - 执行按钮对所有算法统一显示,不再按交互能力隐藏 - 确认按钮仅取消操作选中,交互清理由插件内部处理 - Button 参数统一走 setParam 计数器载荷,移除渲染线程投递路径 - 清理 QRenderWindow 已无调用者的 clearInteraction/postInteractionAction - 同步更新相关注释 --- app/SideBar.qml | 25 ++++--------------------- app/render/QRenderWindow.cpp | 14 -------------- app/render/QRenderWindow.h | 11 ----------- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index 34ca0ce..ca8b483 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -18,14 +18,11 @@ Item{ property var resultText: "" readonly property var activeOp: App.activeOperation - readonly property bool hasInteractive: !!(activeOp && activeOp.info && activeOp.info.interactive) onActiveOpChanged: { parameters = [] resultText = "" // 活动操作声明视口交互能力则激活其交互,否则全部下线(幂等,守卫在功能系统内) - // 必须直接读 activeOp 判定:本处理器与 hasInteractive 绑定由同一变更驱动, - // 求值次序无保证,读绑定可能拿到旧值导致激活被跳过 var interactive = !!(activeOp && activeOp.info && activeOp.info.interactive) QModelManager.featureSystem.setInteractionActive(interactive ? activeOp.info.name : "") } @@ -55,7 +52,6 @@ Item{ Button{ id: commitButton text: "执行" - visible: !root.hasInteractive enabled: !!(root.activeOp && root.activeOp.info) Layout.fillWidth: true onClicked:{ @@ -70,13 +66,8 @@ Item{ text: "确认" enabled: !!(root.activeOp && root.activeOp.info) Layout.fillWidth: true - // 确认 = 结束当前操作:交互功能先及时清理交互系统(标注立即消失), - // 再取消操作选中(停用交互);再次执行需重新点选算法 - onClicked:{ - if (root.hasInteractive && App.registry.renderWindow) - App.registry.renderWindow.clearInteraction() - App.activeOperation = null - } + // 确认 = 结束当前操作,取消操作选中;再次执行需重新点选算法 + onClicked: App.activeOperation = null } } TextArea { @@ -430,18 +421,10 @@ Item{ spacing: 5 width: parameterList.width Button{ - // Button 是无值触发器:交互功能的动作经渲染线程投递(on_action,线程模型约定); - // 普通功能走参数事件广播(计数器载荷,功能约定忽略值只读参数下标) + // Button 是无值触发器:计数器载荷,功能约定忽略值只读参数下标 text: model.name Layout.fillWidth: true - onClicked:{ - if (root.hasInteractive) { - if (App.registry.renderWindow) - App.registry.renderWindow.postInteractionAction(index) - } else { - root.setParam(index, (root.parameters[index] || 0) + 1) - } - } + onClicked: root.setParam(index, (root.parameters[index] || 0) + 1) } } } diff --git a/app/render/QRenderWindow.cpp b/app/render/QRenderWindow.cpp index 596298d..d1ca990 100644 --- a/app/render/QRenderWindow.cpp +++ b/app/render/QRenderWindow.cpp @@ -555,20 +555,6 @@ void QRenderWindow::setComponentEdgeRender(Index component_id, bool is_render) }); } -void QRenderWindow::clearInteraction() -{ - dispatch_async([this](vtkRenderWindow* renderWindow, vtkUserData userData) -> void { - interaction_service_->clear(); - }); -} - -void QRenderWindow::postInteractionAction(int param_index) -{ - dispatch_async([this, param_index](vtkRenderWindow* renderWindow, vtkUserData userData) -> void { - interaction_service_->postAction(param_index); - }); -} - void QRenderWindow::setClick() { dispatch_async([this](vtkRenderWindow* renderWindow, vtkUserData userData) { diff --git a/app/render/QRenderWindow.h b/app/render/QRenderWindow.h index 816a236..1d2a931 100644 --- a/app/render/QRenderWindow.h +++ b/app/render/QRenderWindow.h @@ -111,17 +111,6 @@ public: */ Q_INVOKABLE void setFeatureAdaptor(QObject* adaptor); - /** - * @brief 清空当前交互状态与视图内标注(面板"确认"按钮的会话清理) - */ - Q_INVOKABLE void clearInteraction(); - - /** - * @brief 向当前激活交互投递面板动作按钮事件(Button 参数,渲染线程执行) - * @param param_index 按钮参数下标,作为动作 id 透传给功能 - */ - Q_INVOKABLE void postInteractionAction(int param_index); - /** * @brief 显示/隐藏比例尺(测量插件激活时启用,随相机缩放自动更新刻度) * @param on 是否显示 -- Gitee From 953c32eaced3d7990514c2d69efe984616049527 Mon Sep 17 00:00:00 2001 From: murreletfeather <1941804585@qq.com> Date: Sun, 26 Jul 2026 10:30:19 +0800 Subject: [PATCH 10/13] =?UTF-8?q?refactor(systems,app):=20setInteractionAc?= =?UTF-8?q?tive=20=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA=20setFeatureActive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 语义更准确:意为「启用功能」,自然需要将交互模式打开。 同步更新 Doxygen 注释、日志、测试用例名及 QML 调用处。 --- app/SideBar.qml | 2 +- app/model/systems/feature/QFeatureSystemAdaptor.cpp | 4 ++-- app/model/systems/feature/QFeatureSystemAdaptor.h | 4 ++-- model/systems/feature/FeatureSystem.cpp | 4 ++-- model/systems/feature/FeatureSystem.h | 4 ++-- model/systems/feature/test/TestFeatureSystem.cpp | 12 ++++++------ 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/SideBar.qml b/app/SideBar.qml index ca8b483..40db5e9 100644 --- a/app/SideBar.qml +++ b/app/SideBar.qml @@ -24,7 +24,7 @@ Item{ resultText = "" // 活动操作声明视口交互能力则激活其交互,否则全部下线(幂等,守卫在功能系统内) var interactive = !!(activeOp && activeOp.info && activeOp.info.interactive) - QModelManager.featureSystem.setInteractionActive(interactive ? activeOp.info.name : "") + QModelManager.featureSystem.setFeatureActive(interactive ? activeOp.info.name : "") } // 写入参数值;功能的参数为持久参数,修改即时写回功能系统实时生效 diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.cpp b/app/model/systems/feature/QFeatureSystemAdaptor.cpp index 36d2846..3622e14 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.cpp +++ b/app/model/systems/feature/QFeatureSystemAdaptor.cpp @@ -103,9 +103,9 @@ bool QFeatureSystemAdaptor::postKeyEvent(int key, int modifiers, bool pressed) return feature_system_->dispatchKeyEvent(KeyEvent { key, modifiers, pressed }); } -bool QFeatureSystemAdaptor::setInteractionActive(const QString& unique_name) +bool QFeatureSystemAdaptor::setFeatureActive(const QString& unique_name) { - return feature_system_->setInteractionActive(unique_name.toStdString()); + return feature_system_->setFeatureActive(unique_name.toStdString()); } void QFeatureSystemAdaptor::setActiveModel(int id) diff --git a/app/model/systems/feature/QFeatureSystemAdaptor.h b/app/model/systems/feature/QFeatureSystemAdaptor.h index 5391a2d..d9e43f5 100644 --- a/app/model/systems/feature/QFeatureSystemAdaptor.h +++ b/app/model/systems/feature/QFeatureSystemAdaptor.h @@ -42,10 +42,10 @@ public: */ Q_INVOKABLE bool postKeyEvent(int key, int modifiers, bool pressed); /** - * @brief 设置当前活动功能的交互激活态(活动操作切换驱动,幂等) + * @brief 启用当前活动功能(活动操作切换驱动,幂等) * @param unique_name 要激活的功能唯一名称(须声明 interactive);空串表示全部下线 */ - Q_INVOKABLE bool setInteractionActive(const QString& unique_name); + Q_INVOKABLE bool setFeatureActive(const QString& unique_name); /** * @brief QML侧同步当前活动模型id,供功能上下文动态获取 */ diff --git a/model/systems/feature/FeatureSystem.cpp b/model/systems/feature/FeatureSystem.cpp index 0890433..b146995 100644 --- a/model/systems/feature/FeatureSystem.cpp +++ b/model/systems/feature/FeatureSystem.cpp @@ -161,7 +161,7 @@ interaction::InteractionState* FeatureSystem::activeInteraction() return nullptr; } -bool FeatureSystem::setInteractionActive(const std::string& unique_name) +bool FeatureSystem::setFeatureActive(const std::string& unique_name) { // 空串 = 活动操作无交互能力:全部下线(重复调用由 InteractionContext 的目标状态守卫兜底) if (unique_name.empty()) { @@ -172,7 +172,7 @@ bool FeatureSystem::setInteractionActive(const std::string& unique_name) auto it = entries_.find(unique_name); if (it == entries_.end() || !it->second.info->interactive) { - spdlog::warn("FeatureSystem::setInteractionActive: feature '{}' not found or not interactive", unique_name); + spdlog::warn("FeatureSystem::setFeatureActive: feature '{}' not found or not interactive", unique_name); return false; } it->second.interaction_context.setActive(true); // 单激活约定:自动下线其他功能 diff --git a/model/systems/feature/FeatureSystem.h b/model/systems/feature/FeatureSystem.h index 41e4c07..57b2910 100644 --- a/model/systems/feature/FeatureSystem.h +++ b/model/systems/feature/FeatureSystem.h @@ -94,14 +94,14 @@ public: */ interaction::InteractionState* activeInteraction(); /** - * @brief 设置当前活动功能的交互激活态(声明 interactive 的功能专用,活动操作切换驱动) + * @brief 启用当前活动功能(声明 interactive 的功能专用,活动操作切换驱动) * * 单激活约定:激活一个功能会经 InteractionContext 下线其他功能的交互; * 启停为幂等的状态应用(InteractionContext 以目标状态为守卫),重复设置无副作用。 * @param unique_name 要激活的功能唯一名称;空串表示全部下线(活动操作无交互能力) * @return 名称为空,或功能存在且声明 interactive 时为 true */ - bool setInteractionActive(const std::string& unique_name); + bool setFeatureActive(const std::string& unique_name); /** * @brief 设置功能信息变更回调函数 */ diff --git a/model/systems/feature/test/TestFeatureSystem.cpp b/model/systems/feature/test/TestFeatureSystem.cpp index 015244d..9fbed1d 100644 --- a/model/systems/feature/test/TestFeatureSystem.cpp +++ b/model/systems/feature/test/TestFeatureSystem.cpp @@ -314,7 +314,7 @@ TEST_CASE("FeatureSystem::activeInteraction tracks interactive activation", "[Fe REQUIRE(system.activeInteraction() == nullptr); } -TEST_CASE("FeatureSystem::setInteractionActive drives activation by feature name", "[FeatureSystem]") +TEST_CASE("FeatureSystem::setFeatureActive drives activation by feature name", "[FeatureSystem]") { core::EventBus bus; ModelLayer model_layer; @@ -332,18 +332,18 @@ TEST_CASE("FeatureSystem::setInteractionActive drives activation by feature name REQUIRE(system.registerHandler(plain_meta, std::move(plain))); // 未注册功能与未声明 interactive 的功能不可激活 - REQUIRE_FALSE(system.setInteractionActive("Unknown")); - REQUIRE_FALSE(system.setInteractionActive("PlainFeature")); + REQUIRE_FALSE(system.setFeatureActive("Unknown")); + REQUIRE_FALSE(system.setFeatureActive("PlainFeature")); REQUIRE(system.activeInteraction() == nullptr); // 按名激活(幂等:重复设置无副作用) - REQUIRE(system.setInteractionActive("InteractiveFeature")); - REQUIRE(system.setInteractionActive("InteractiveFeature")); + REQUIRE(system.setFeatureActive("InteractiveFeature")); + REQUIRE(system.setFeatureActive("InteractiveFeature")); auto* active = system.activeInteraction(); REQUIRE(active != nullptr); REQUIRE(active->active); // 活动操作切到无交互能力的功能:空串全部下线 - REQUIRE(system.setInteractionActive("")); + REQUIRE(system.setFeatureActive("")); REQUIRE(system.activeInteraction() == nullptr); } -- Gitee From 7705d833b2cca161b1a5ae92c940e5790defb303 Mon Sep 17 00:00:00 2001 From: a-ciue Date: Wed, 29 Jul 2026 15:30:12 +0800 Subject: [PATCH 11/13] =?UTF-8?q?refact(interact):=20=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=B8=8E=E6=B8=85=E7=90=86=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E6=B8=85=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构交互状态刷新与清理流程,移除 on_clear/on_action,统一由参数变更事件驱动“清除”按钮逻辑。引入 atomic 标志与 deferred_op,支持跨线程安全同步与延迟操作。FeatureSystem 注入渲染刷新回调,功能层 requestRefresh 可驱动视口重绘。测量功能逻辑简化,文本精度统一,相关测试用例同步调整。整体提升线程安全性与功能/渲染层解耦。 --- app/model/QArgObject.cpp | 4 ++ app/render/InteractionService.cpp | 26 +++---- app/render/InteractionService.h | 8 +-- app/render/QRenderWindow.cpp | 20 ++++++ app/render/QRenderWindow.h | 2 + app/render/test/TestInteractionService.cpp | 27 +++----- model/systems/core/InteractionState.h | 12 ++-- model/systems/feature/FeatureSystem.cpp | 10 +++ model/systems/feature/FeatureSystem.h | 5 ++ model/systems/feature/InteractionContext.cpp | 29 +++++--- model/systems/feature/InteractionContext.h | 18 +++-- .../feature/test/TestFeatureSystem.cpp | 46 +++++++++++++ .../feature/MeasurePlugin/MeasureHandler.cpp | 67 ++++++++++--------- .../feature/MeasurePlugin/MeasureHandler.h | 15 ++--- .../MeasurePlugin/test/TestMeasureHandler.cpp | 31 +++++---- 15 files changed, 211 insertions(+), 109 deletions(-) diff --git a/app/model/QArgObject.cpp b/app/model/QArgObject.cpp index 2ee8cf9..d8cca96 100644 --- a/app/model/QArgObject.cpp +++ b/app/model/QArgObject.cpp @@ -46,6 +46,10 @@ std::optional QArgObject::getValue() const } break; } + case ArgTypeEnum::Button: + // Button 是无值触发器:计数器载荷,功能约定忽略值只读参数下标 + ret = ArgObject::create(value_.toInt(&canConvert)); + break; default: canConvert = false; break; diff --git a/app/render/InteractionService.cpp b/app/render/InteractionService.cpp index 3a3e35a..30abcdc 100644 --- a/app/render/InteractionService.cpp +++ b/app/render/InteractionService.cpp @@ -197,22 +197,18 @@ void InteractionService::hover(double posx, double posy) } } -void InteractionService::clear() +void InteractionService::syncPending() { - if (!current_) - return; - if (current_->on_clear) - current_->on_clear(); - refreshAnnotations(); -} - -void InteractionService::postAction(int param_index) -{ - if (!current_) - return; - if (current_->on_action) - current_->on_action(param_index); - refreshAnnotations(); + // 激活迁移:GUI 线程 setActive 经 notify 到达,syncState 执行上线/下线(幂等,无迁移时零成本) + syncState(); + if (current_ && current_->needs_refresh.exchange(false)) { + // GUI 线程经 deferred_op 延迟的操作,在渲染线程安全执行 + if (current_->deferred_op) { + current_->deferred_op(); + current_->deferred_op = nullptr; + } + refreshAnnotations(); + } } void InteractionService::clearActors() diff --git a/app/render/InteractionService.h b/app/render/InteractionService.h index 4d6d29a..e75d60d 100644 --- a/app/render/InteractionService.h +++ b/app/render/InteractionService.h @@ -48,11 +48,9 @@ public: void pick(double posx, double posy); //! @brief 悬停:同步激活状态,调用 on_hover 回调做动态预览 void hover(double posx, double posy); - //! @brief 面板"确认"按钮:调用当前状态的 on_clear 回调并刷新标注 - void clear(); - //! @brief 面板动作按钮(Button 参数):调用当前状态的 on_action 回调并刷新标注 - //! @param param_index 按钮参数下标,作为动作 id 透传给功能 - void postAction(int param_index); + //! @brief 由 notify 回调驱动(GUI 线程 setActive/requestRefresh 后):同步激活迁移并拉取标注刷新 VTK actor + void syncPending(); + private: //! @brief 同步激活状态:迁移时执行下线(on_deactivate/清标注/还原吸附)与上线(吸附/on_activate/刷新) systems::interaction::InteractionState* syncState(); diff --git a/app/render/QRenderWindow.cpp b/app/render/QRenderWindow.cpp index d1ca990..8388054 100644 --- a/app/render/QRenderWindow.cpp +++ b/app/render/QRenderWindow.cpp @@ -161,6 +161,8 @@ QQuickVTKItem::vtkUserData QRenderWindow::initializeVTK(vtkRenderWindow* renderW auto* feature_system = feature_adaptor_ ? feature_adaptor_->featureSystem() : nullptr; return feature_system ? feature_system->activeInteraction() : nullptr; }; + // 注入渲染刷新回调(与 setFeatureAdaptor 互补,确保两者先后顺序均能生效) + injectRenderRefreshCallback(); vtk->orientationWidget->AnimateOff(); vtk->orientationWidget->SetParentRenderer(vtk->renderer_); @@ -512,6 +514,8 @@ void QRenderWindow::clearSelection() void QRenderWindow::setFeatureAdaptor(QObject* adaptor) { feature_adaptor_ = qobject_cast(adaptor); + // 注入渲染刷新回调:功能经 requestRefresh() 触发 VTK 重绘 + injectRenderRefreshCallback(); } void QRenderWindow::setScaleBarVisible(bool on) @@ -618,3 +622,19 @@ void QRenderWindow::cancelAttri() } vtkStandardNewMacro(QRenderWindow::Data); + +void QRenderWindow::injectRenderRefreshCallback() +{ + if (!feature_adaptor_ || !feature_adaptor_->featureSystem() || !interaction_service_) + return; + auto refresh = [this]() { + dispatch_async([this](vtkRenderWindow* rw, vtkUserData) { + interaction_service_->syncPending(); // 同步激活迁移并拉取标注到 VTK actor + rw->Render(); + }); + }; + feature_adaptor_->featureSystem()->setRenderRefreshCallback(refresh); + // 注入即排空一次:requestRefresh 的合并跳过以"置位必有在途消费者"为前提, + // 兜底注入前(回调缺失期)置位而未被消费的挂起刷新 + refresh(); +} diff --git a/app/render/QRenderWindow.h b/app/render/QRenderWindow.h index 1d2a931..8b0085c 100644 --- a/app/render/QRenderWindow.h +++ b/app/render/QRenderWindow.h @@ -210,5 +210,7 @@ private: QModelQuery* model_query_ {}; void updateGlobalVtkPointsImpl(Data* vtk); + //! @brief 注入渲染刷新回调到 FeatureSystem(initializeVTK 与 setFeatureAdaptor 各调一次,确保初始化顺序无关) + void injectRenderRefreshCallback(); }; #endif // Q_RENDER_WINDOW_H diff --git a/app/render/test/TestInteractionService.cpp b/app/render/test/TestInteractionService.cpp index 07344ac..d4d8ecd 100644 --- a/app/render/test/TestInteractionService.cpp +++ b/app/render/test/TestInteractionService.cpp @@ -49,9 +49,6 @@ struct FakeInteraction { int activate_count = 0; int deactivate_count = 0; - int clear_count = 0; - int action_count = 0; - int last_action = -1; int hover_count = 0; std::vector picks; @@ -59,11 +56,6 @@ struct FakeInteraction { { state.on_activate = [this] { ++activate_count; }; state.on_deactivate = [this] { ++deactivate_count; }; - state.on_clear = [this] { ++clear_count; }; - state.on_action = [this](int param_index) { - ++action_count; - last_action = param_index; - }; state.on_pick = [this](const PickInfo& pick) { picks.push_back(pick); return true; @@ -218,14 +210,6 @@ int main(int argc, char* argv[]) service.pick(5000, 5000); // 窗口外坐标,拾取器必然未命中 check(fake.picks.size() == picks_before, "未命中吸附点时不调用 onPick"); - // ---- clear:面板"确认"的会话清理转发到处理器 ---- - service.clear(); - check(fake.clear_count == 1, "clear() 转发到处理器"); - - // ---- postAction:面板动作按钮转发到处理器并携带参数下标 ---- - service.postAction(2); - check(fake.action_count == 1 && fake.last_action == 2, "postAction() 转发到处理器并携带参数下标"); - // ---- 几何(OCC)顶点拾取:geom_id 填充 ---- { TopoDS_Shape box = BRepPrimAPI_MakeBox(gp_Pnt(500.0, 500.0, 0.0), 1000.0, 800.0, 600.0).Shape(); @@ -261,6 +245,17 @@ int main(int argc, char* argv[]) check(fake.deactivate_count == 1, "下线时调用 onDeactivate 一次"); check(!service.hasActiveState(), "开关关闭后无激活交互"); + // ---- syncPending:模拟 GUI 线程 setActive 置位 + notify 到达,驱动激活迁移 ---- + interaction_enabled = true; + fake.state.needs_refresh = true; // setActive(true):置位 + notify + service.syncPending(); + check(fake.activate_count == 2, "syncPending 驱动上线(onActivate 再次调用)"); + interaction_enabled = false; + fake.state.needs_refresh = true; // setActive(false):置位 + notify + service.syncPending(); + check(fake.deactivate_count == 2, "syncPending 驱动下线(onDeactivate 再次调用)"); + check(!service.hasActiveState(), "syncPending 下线后无激活交互"); + spdlog::info("==== InteractionService self-check: {} ====", g_failures == 0 ? "ALL PASS" : "HAS FAILURES"); diff --git a/model/systems/core/InteractionState.h b/model/systems/core/InteractionState.h index 30406a5..099804d 100644 --- a/model/systems/core/InteractionState.h +++ b/model/systems/core/InteractionState.h @@ -7,6 +7,7 @@ #include "InteractiveTypes.h" +#include #include namespace systems::interaction { @@ -17,21 +18,24 @@ namespace systems::interaction { * 线程约定(与 EventBus 不同,修改前必读): * - 全部回调由 **渲染线程**调用(InteractionService 经 dispatch_async 驱动); * - 功能在 **GUI 线程** 的 activate() 中经 InteractionContext 注册回调与初始状态; + * - GUI 线程的交互状态/标注变更经 setActive/requestRefresh 置位 + notify, + * 渲染线程 syncPending 承接激活迁移与标注拉取(跨线程标志为 atomic); * - 注册先行、调用在后,会话启停与选择系统的互斥由 UI 层保证(同原 InteractiveHandler 约定)。 */ struct InteractionState { - bool active = false; //> 交互激活态:功能经 setActive 写入(GUI 线程),渲染层读取路由拾取 + std::atomic active { false }; //> 交互激活态:功能经 setActive 写入(GUI 线程),渲染层读取路由拾取 + std::atomic needs_refresh { false }; //> GUI 线程变更交互状态/标注后置位,渲染层拉取后复位 std::function on_activate; //> 交互会话开始(通常清空功能内部状态) std::function on_deactivate; //> 交互会话结束 - std::function on_clear; //> 面板"确认"按钮的会话清理:清空当前会话状态 - //! @brief 面板动作按钮(Button 参数):参数下标为动作 id,动作语义由功能自定义 - std::function on_action; //! @brief 左键拾取:返回是否有状态变化(需要刷新标注) std::function on_pick; //! @brief 悬停:返回是否更新了预览(需要刷新标注) std::function on_hover; AnnotationBatch annotations; //> 功能在回调中直接更新,渲染层事件后拉取绘制 + //! @brief GUI 线程延迟操作:渲染线程 syncPending 消费执行后拉取标注刷新 + //!(覆盖语义:重复置位仅保留最新,操作须幂等) + std::function deferred_op; //! @brief 会话结束时的渲染层清理:清空标注(不动订阅,订阅随功能常驻) void clearSession() diff --git a/model/systems/feature/FeatureSystem.cpp b/model/systems/feature/FeatureSystem.cpp index b146995..3078346 100644 --- a/model/systems/feature/FeatureSystem.cpp +++ b/model/systems/feature/FeatureSystem.cpp @@ -79,6 +79,11 @@ bool FeatureSystem::registerHandler(const HandlerMetaData& meta_data, SystemHand } } }; + // 注入渲染刷新回调:功能经 requestRefresh() 通知 app 层拉取标注并重绘视口 + entry.interaction_context.render_refresh_ = [this] { + if (render_refresh_callback_) + render_refresh_callback_(); + }; // 激活失败则撤掉整个条目,不留下半注册状态 try { @@ -194,6 +199,11 @@ void FeatureSystem::setActiveComponentProvider(std::function callback) +{ + render_refresh_callback_ = std::move(callback); +} + bool FeatureSystem::dispatchKeyEvent(const KeyEvent& event) { // 原始事件流先广播,观察者总能收到;再做按键绑定路由并返回消费结果 diff --git a/model/systems/feature/FeatureSystem.h b/model/systems/feature/FeatureSystem.h index 57b2910..74c3186 100644 --- a/model/systems/feature/FeatureSystem.h +++ b/model/systems/feature/FeatureSystem.h @@ -112,6 +112,10 @@ public: */ void setActiveModelProvider(std::function()> provider); void setActiveComponentProvider(std::function()> provider); + /** + * @brief 设置视口渲染刷新回调(app 层注入,功能经 InteractionContext::requestRefresh 触发) + */ + void setRenderRefreshCallback(std::function callback); private: struct FeatureEntry { @@ -131,6 +135,7 @@ private: std::function()> active_model_provider_; std::function()> active_component_provider_; std::function on_feature_infos_changed_; + std::function render_refresh_callback_; //> app 层注入:通知渲染窗口拉取标注并重绘 }; } #endif // FEATURE_SYSTEM_H diff --git a/model/systems/feature/InteractionContext.cpp b/model/systems/feature/InteractionContext.cpp index 27a196c..517e71c 100644 --- a/model/systems/feature/InteractionContext.cpp +++ b/model/systems/feature/InteractionContext.cpp @@ -21,16 +21,6 @@ void InteractionContext::onDeactivate(std::function cb) state_->on_deactivate = std::move(cb); } -void InteractionContext::onClear(std::function cb) -{ - state_->on_clear = std::move(cb); -} - -void InteractionContext::onAction(std::function cb) -{ - state_->on_action = std::move(cb); -} - void InteractionContext::onPick(std::function cb) { state_->on_pick = std::move(cb); @@ -46,6 +36,23 @@ systems::interaction::AnnotationBatch& InteractionContext::annotations() return state_->annotations; } +void InteractionContext::requestRefresh() +{ + // 合并语义:needs_refresh 仍为 true 说明渲染侧尚未消费、必有在途 syncPending, + // 跳过重复 notify,由在途刷新一并拉取(负载先于置位写入,消费者可见最新值) + if (state_->needs_refresh.exchange(true)) + return; + if (render_refresh_) + render_refresh_(); +} + +void InteractionContext::deferRefresh(std::function pre_op) +{ + // 负载先于置位写入:渲染线程消费 deferred_op 时可见最新操作 + state_->deferred_op = std::move(pre_op); + requestRefresh(); +} + void InteractionContext::setActive(bool on) { // 幂等守卫:目标状态已达则直接返回,重复启停无副作用 @@ -56,6 +63,8 @@ void InteractionContext::setActive(bool on) deactivate_others_(); } state_->active = on; + // 启停均通知渲染层:syncPending 经 syncState 承接迁移(上线 on_activate/吸附切换,下线 on_deactivate/清理) + requestRefresh(); } } // namespace systems::feature diff --git a/model/systems/feature/InteractionContext.h b/model/systems/feature/InteractionContext.h index 2eb1614..3a8014c 100644 --- a/model/systems/feature/InteractionContext.h +++ b/model/systems/feature/InteractionContext.h @@ -26,10 +26,6 @@ public: void onActivate(std::function cb); //! @brief 订阅交互会话结束(渲染线程) void onDeactivate(std::function cb); - //! @brief 订阅会话清理(面板"确认"按钮,渲染线程) - void onClear(std::function cb); - //! @brief 订阅面板动作按钮(Button 参数,渲染线程;参数下标为动作 id,语义由功能自定义) - void onAction(std::function cb); //! @brief 订阅左键拾取(渲染线程;返回是否有状态变化需要刷新标注) void onPick(std::function cb); //! @brief 订阅悬停(渲染线程;返回是否更新预览需要刷新标注) @@ -38,13 +34,25 @@ public: //! @brief 标注集:功能在回调中直接更新,渲染层拉取绘制 systems::interaction::AnnotationBatch& annotations(); + //! @brief GUI 线程交互状态/标注变更后请求渲染刷新(渲染层 syncPending 拉取并复位 needs_refresh) + //! @note 合并语义:needs_refresh 已置位(渲染侧尚未消费)时跳过重复 notify,由在途刷新一并拉取 + void requestRefresh(); + //! @brief GUI 线程延迟刷新:将刷新前置操作存入 InteractionState,经 requestRefresh 通知渲染线程执行后拉取标注 + //! @param pre_op 刷新前需在渲染线程执行的操作(如清理功能交互状态;覆盖语义,须幂等) + void deferRefresh(std::function pre_op); + //! @brief 设置本功能交互激活态(单激活:激活自己时会先下线其他功能的交互) void setActive(bool on); +private: + // 装配注入仅 FeatureSystem 可用:功能只调用、不可覆盖系统接线(冻结后渲染线程调用无重赋值竞争) + friend class FeatureSystem; + //! @brief 由 FeatureSystem 装配时注入:激活本功能前下线其他功能的交互(单激活约定) std::function deactivate_others_; + //! @brief 由 FeatureSystem 装配时注入:通知渲染层拉取标注并刷新视口 + std::function render_refresh_; -private: systems::interaction::InteractionState* state_; }; diff --git a/model/systems/feature/test/TestFeatureSystem.cpp b/model/systems/feature/test/TestFeatureSystem.cpp index 9fbed1d..b927135 100644 --- a/model/systems/feature/test/TestFeatureSystem.cpp +++ b/model/systems/feature/test/TestFeatureSystem.cpp @@ -347,3 +347,49 @@ TEST_CASE("FeatureSystem::setFeatureActive drives activation by feature name", " REQUIRE(system.setFeatureActive("")); REQUIRE(system.activeInteraction() == nullptr); } + +TEST_CASE("InteractionContext::setActive notifies render refresh in both directions", "[FeatureSystem]") +{ + core::EventBus bus; + ModelLayer model_layer; + FeatureSystem system(model_layer, bus); + + auto meta = makeMetaData(); + meta.name = "NotifyTest"; + meta.interactive = true; + auto* raw = new FakeFeatureHandler; + FeatureSystem::SystemHandlerPtr handler { raw }; + REQUIRE(system.registerHandler(meta, std::move(handler))); + + int notify_count = 0; + system.setRenderRefreshCallback([¬ify_count] { ++notify_count; }); + + // 激活:置位 needs_refresh 并 notify(渲染层 syncPending 经 syncState 上线) + raw->context->interaction.setActive(true); + auto* state = system.activeInteraction(); + REQUIRE(state != nullptr); + CHECK(state->needs_refresh); + CHECK(notify_count == 1); + + // 停用:同样置位 + notify(渲染层 syncPending 检测迁移并执行下线清理) + state->needs_refresh = false; + raw->context->interaction.setActive(false); + CHECK_FALSE(state->active); + CHECK(state->needs_refresh); + CHECK(notify_count == 2); + + // 幂等守卫:重复调用不重复置位/通知 + state->needs_refresh = false; + raw->context->interaction.setActive(false); + CHECK_FALSE(state->needs_refresh); + CHECK(notify_count == 2); + + // 合并语义:挂起刷新未消费时重复 requestRefresh 跳过 notify,消费后可再次 notify + raw->context->interaction.requestRefresh(); + CHECK(notify_count == 3); + raw->context->interaction.requestRefresh(); + CHECK(notify_count == 3); + state->needs_refresh = false; // 模拟渲染层消费 + raw->context->interaction.requestRefresh(); + CHECK(notify_count == 4); +} diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.cpp b/plugins/feature/MeasurePlugin/MeasureHandler.cpp index 7d6e702..0ff99ea 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/MeasureHandler.cpp @@ -5,7 +5,9 @@ */ #include "MeasureHandler.h" +#include "EventBus.h" #include "FeatureContext.h" +#include "FeatureEvents.h" #include "FeatureRegistrar.h" #include "InteractionContext.h" @@ -21,6 +23,8 @@ namespace systems::feature { namespace { using Vec3 = std::array; +constexpr const char* kFeatureName = "MeasurePlugin"; //> 插件 json 注册名,过滤 ParameterChangedEvent 用 + constexpr double kEps = 1e-9; Vec3 operator-(const Vec3& a, const Vec3& b) @@ -44,11 +48,12 @@ double length(const Vec3& v) return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); } -std::string toString(double value, int precision = 6) +//! @brief 数值转两位小数字符串(测量值显示精度) +std::string toString(double value) { std::ostringstream oss; oss.setf(std::ios::fixed, std::ios::floatfield); - oss.precision(precision); + oss.precision(2); oss << value; return oss.str(); } @@ -64,11 +69,21 @@ double angleBetween(const Vec3& u, const Vec3& v) cos_theta = std::max(-1.0, std::min(1.0, cos_theta)); return std::acos(cos_theta) * 180.0 / 3.14159265358979323846; } + +//! @brief 两点是否同一吸附点:全局顶点 id 优先,同源顶点坐标位级一致兜底 +bool samePoint(const systems::interaction::PickInfo& a, const systems::interaction::PickInfo& b) +{ + if (a.mesh_id >= 0 && a.mesh_id == b.mesh_id) + return true; + if (a.geom_id >= 0 && a.geom_id == b.geom_id) + return true; + return a.world_pos == b.world_pos; +} } void MeasureHandler::setup(FeatureRegistrar& reg) { - // "清除"按钮参数:无值触发器,点击经 on_action 于渲染线程回到本功能 + // "清除"按钮参数:无值触发器,点击发布 ParameterChangedEvent,功能内部订阅并清空 reg.addParameter({ ArgTypeEnum::Button, "清除", "" }); reg.addMenuItem({ "工具", "测量" }); } @@ -79,24 +94,20 @@ void MeasureHandler::activate(FeatureContext& ctx) annotations_ = &ctx.interaction.annotations(); ctx.interaction.onActivate([this]() { this->clear(); }); ctx.interaction.onDeactivate([this]() { this->clear(); }); - ctx.interaction.onClear([this]() { this->clear(); }); - ctx.interaction.onAction([this](int) { this->clear(); }); ctx.interaction.onPick([this](const PickInfo& p) { return this->onPick(p); }); ctx.interaction.onHover([this](const PickInfo& p) { return this->onHover(p); }); + + // "清除"按钮经 ParameterChangedEvent 触发:清理作为刷新前置操作,延迟到渲染线程安全执行 + param_sub_ = ctx.events.subscribe([this, interaction = &ctx.interaction](const ParameterChangedEvent& e) { + // 按功能名过滤:其他功能的参数变更不触发本功能清空 + if (e.feature != kFeatureName || e.param_index != 0) + return; + interaction->deferRefresh([this] { this->clear(); }); + }); } // ---------------- 交互回调(经 InteractionContext 注册,渲染线程驱动) ---------------- -bool MeasureHandler::samePoint(const PickInfo& a, const PickInfo& b) -{ - if (a.mesh_id >= 0 && a.mesh_id == b.mesh_id) - return true; - if (a.geom_id >= 0 && a.geom_id == b.geom_id) - return true; - // 同源顶点再次拾取坐标位级一致,作无 id 时的兜底 - return a.world_pos == b.world_pos; -} - bool MeasureHandler::onPick(const PickInfo& pick) { if (!pick.valid) @@ -119,14 +130,13 @@ bool MeasureHandler::onHover(const PickInfo& pick) { // 未吸附或无起笔:清除已有预览;本来无预览则无需刷新 if (!pending_ || !pick.valid) { - if (!has_preview_) + if (!preview_) return false; - has_preview_ = false; + preview_.reset(); refreshAnnotations(); return true; } - has_preview_ = true; preview_ = pick; refreshAnnotations(); return true; @@ -141,16 +151,12 @@ void MeasureHandler::addLine(const PickInfo& a, const PickInfo& b) } // 与每条已有线做端点匹配,共端点即记录一组夹角 - const int new_idx = static_cast(lines_.size()); - for (size_t i = 0; i < lines_.size(); ++i) { - const MeasureLine& l = lines_[i]; + for (const MeasureLine& l : lines_) { auto try_share = [&](const PickInfo& old_shared, const PickInfo& old_other, const PickInfo& new_shared, const PickInfo& new_other) { if (!samePoint(old_shared, new_shared)) return; MeasureAngle ang; - ang.line1 = static_cast(i); - ang.line2 = new_idx; ang.at = old_shared.world_pos; ang.p = old_other.world_pos; ang.q = new_other.world_pos; @@ -181,7 +187,7 @@ void MeasureHandler::refreshAnnotations() // 长度文本:每线一个,放线段中点(白色) for (const MeasureLine& l : lines_) { const Vec3 mid = midpoint(l.a.world_pos, l.b.world_pos); - annotations_->texts.push_back({ mid, "L: " + toString(length(l.b.world_pos - l.a.world_pos), 2), 1.0, 1.0, 1.0 }); + annotations_->texts.push_back({ mid, "L: " + toString(length(l.b.world_pos - l.a.world_pos)), 1.0, 1.0, 1.0 }); } // 夹角文本:放共点沿角平分线偏移(青色);同一点多个夹角按序号加大偏移防重叠 @@ -214,32 +220,33 @@ void MeasureHandler::refreshAnnotations() const double dist = 0.25 * std::min(lu, lv) * (1.0 + 0.3 * stack); annotations_->texts.push_back({ { ang.at[0] + dir[0] * dist, ang.at[1] + dir[1] * dist, ang.at[2] + dir[2] * dist }, - "Ang: " + toString(ang.angle, 2), 0.3, 0.9, 1.0 }); + "Ang: " + toString(ang.angle), 0.3, 0.9, 1.0 }); } // 悬停动态预览:黄色虚线 + 黄色长度文本 - if (pending_ && has_preview_) { + if (pending_ && preview_) { AnnotationLine preview; preview.p0 = pending_->world_pos; - preview.p1 = preview_.world_pos; + preview.p1 = preview_->world_pos; preview.r = 1.0; preview.g = 0.9; preview.b = 0.1; preview.dashed = true; annotations_->lines.push_back(preview); - const Vec3 mid = midpoint(pending_->world_pos, preview_.world_pos); + const Vec3 mid = midpoint(pending_->world_pos, preview_->world_pos); annotations_->texts.push_back({ mid, - "L: " + toString(length(preview_.world_pos - pending_->world_pos), 2), 1.0, 0.9, 0.1 }); + "L: " + toString(length(preview_->world_pos - pending_->world_pos)), 1.0, 0.9, 0.1 }); } } void MeasureHandler::clear() { pending_.reset(); - has_preview_ = false; + preview_.reset(); lines_.clear(); angles_.clear(); + // 调用方(on_activate/on_deactivate/deferRefresh 前置操作)均在渲染线程,重建空标注后由框架拉取刷新 refreshAnnotations(); } diff --git a/plugins/feature/MeasurePlugin/MeasureHandler.h b/plugins/feature/MeasurePlugin/MeasureHandler.h index 7788658..21ae844 100644 --- a/plugins/feature/MeasurePlugin/MeasureHandler.h +++ b/plugins/feature/MeasurePlugin/MeasureHandler.h @@ -4,6 +4,7 @@ * @author 范成通 email 1941804585@qq.com */ #pragma once +#include "EventBus.h" #include "FeatureHandler.h" #include "InteractiveTypes.h" #include @@ -19,8 +20,9 @@ using systems::interaction::PickInfo; /** * @brief 测量处理器:交互式两点成线测距与共端点两线夹角标注 * - * 纯视口交互功能:无参数、无执行路径;交互回调(拾取/悬停/激活/停用/清除) - * 由渲染线程经 InteractionService 驱动,全部状态成员仅被交互回调访问。 + * 纯视口交互功能:"清除"按钮参数经 ParameterChangedEvent 触发清空, + * 交互回调(拾取/悬停/激活/停用)由渲染线程经 InteractionService 驱动, + * 全部状态成员仅被交互回调访问。 */ class MeasureHandler : public FeatureHandler { public: @@ -29,7 +31,7 @@ public: //! @brief 声明"清除"按钮参数与菜单项 void setup(FeatureRegistrar& reg) override; - //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用/会话清理/动作按钮) + //! @brief 激活:经 ctx.interaction 注册交互回调(拾取/悬停/激活/停用),订阅参数变更事件 void activate(FeatureContext& ctx) override; //! @brief 交互测量状态查询(测试与面板用) @@ -48,26 +50,23 @@ private: }; //! @brief 共端点两线的夹角:at 为共点,p/q 为两线各自另一端点 struct MeasureAngle { - int line1 = 0; //> lines_ 下标 - int line2 = 0; std::array at; std::array p; std::array q; double angle = 0.0; }; - static bool samePoint(const PickInfo& a, const PickInfo& b); void addLine(const PickInfo& a, const PickInfo& b); //! @brief 由当前状态重建标注集 void refreshAnnotations(); std::optional pending_; //> 已起笔未成线的首点 - bool has_preview_ = false; //> 悬停是否正在预览 - PickInfo preview_ {}; //> 悬停预览吸附点 + std::optional preview_; //> 悬停预览吸附点 std::vector lines_; std::vector angles_; // 标注集契约:功能在回调中直接更新 InteractionState.annotations(activate 时绑定), // 渲染层事件后拉取绘制;自持成员会导致渲染层永远拉到空标注 AnnotationBatch* annotations_ { nullptr }; + core::EventBus::Subscription param_sub_; //> ParameterChangedEvent 订阅句柄(析构自动退订) }; } diff --git a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp index 04c03d5..ed6edf8 100644 --- a/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp +++ b/plugins/feature/MeasurePlugin/test/TestMeasureHandler.cpp @@ -7,6 +7,7 @@ #include "MeasureHandler.h" #include "EventBus.h" #include "FeatureContext.h" +#include "FeatureEvents.h" #include "FeatureParams.h" #include "FeatureRegistrar.h" #include "InteractionContext.h" @@ -66,7 +67,7 @@ TEST_CASE("MeasureHandler setup declares clear button parameter and menu") REQUIRE(reg.menuItems().size() == 1); } -TEST_CASE("MeasureHandler: interactive picks update state annotations and onClear resets") +TEST_CASE("MeasureHandler: interactive picks update state annotations and ParameterChangedEvent clears") { FeatureTestEnv env; env.handler.activate(*env.ctx); @@ -92,21 +93,19 @@ TEST_CASE("MeasureHandler: interactive picks update state annotations and onClea CHECK(env.interaction_state_.annotations.points.size() == 2); CHECK(env.interaction_state_.annotations.texts.size() == 1); - // 面板"确认"的会话清理:on_clear 清空会话状态与标注 - REQUIRE(env.interaction_state_.on_clear); - env.interaction_state_.on_clear(); - CHECK(env.handler.lineCount() == 0); - CHECK(!env.handler.hasPending()); - CHECK(env.interaction_state_.annotations.lines.empty()); - CHECK(env.interaction_state_.annotations.points.empty()); - CHECK(env.interaction_state_.annotations.texts.empty()); - - // 面板动作按钮("清除"参数):on_action 同样清空会话状态与标注 - env.interaction_state_.on_pick(p1); - env.interaction_state_.on_pick(p2); - CHECK(env.handler.lineCount() == 1); - REQUIRE(env.interaction_state_.on_action); - env.interaction_state_.on_action(0); + // 其他功能的参数变更不触发本功能清空(按功能名过滤,注册名见插件 json) + env.bus.publish(ParameterChangedEvent { "OtherFeature", 0, core::ArgObject {} }); + CHECK_FALSE(env.interaction_state_.needs_refresh); + CHECK_FALSE(env.interaction_state_.deferred_op); + + // 面板"清除"按钮参数:通过 ParameterChangedEvent 触发延迟清空(GUI 线程置位,渲染线程执行) + env.bus.publish(ParameterChangedEvent { "MeasurePlugin", 0, core::ArgObject {} }); + // GUI 线程只置位延迟清空回调与刷新标志,不直接修改功能状态 + CHECK(env.interaction_state_.needs_refresh); + REQUIRE(env.interaction_state_.deferred_op); + // 模拟渲染线程 syncPending:执行延迟清空后检查状态 + env.interaction_state_.deferred_op(); + env.interaction_state_.deferred_op = nullptr; CHECK(env.handler.lineCount() == 0); CHECK(!env.handler.hasPending()); CHECK(env.interaction_state_.annotations.lines.empty()); -- Gitee From 71da80235a53cf0e27e698e7160b20b0c0d01a84 Mon Sep 17 00:00:00 2001 From: a-ciue Date: Wed, 29 Jul 2026 16:11:05 +0800 Subject: [PATCH 12/13] =?UTF-8?q?fix(interact):=20=E4=BF=AE=E5=A4=8Dintera?= =?UTF-8?q?ct=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E6=97=B6=E6=B8=85=E7=A9=BA=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/render/test/TestInteractionService.cpp | 13 +++++++++++++ model/systems/core/InteractionState.h | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/render/test/TestInteractionService.cpp b/app/render/test/TestInteractionService.cpp index d4d8ecd..f5f0ed0 100644 --- a/app/render/test/TestInteractionService.cpp +++ b/app/render/test/TestInteractionService.cpp @@ -256,6 +256,19 @@ int main(int argc, char* argv[]) check(fake.deactivate_count == 2, "syncPending 驱动下线(onDeactivate 再次调用)"); check(!service.hasActiveState(), "syncPending 下线后无激活交互"); + // ---- 下线即消费挂起状态:needs_refresh/deferred_op 随 clearSession 失效,不堵死后续 notify ---- + interaction_enabled = true; + fake.state.needs_refresh = true; + service.syncPending(); + check(fake.activate_count == 3, "syncPending 再次驱动上线"); + interaction_enabled = false; + fake.state.needs_refresh = true; // setActive(false) 置位 + fake.state.deferred_op = [] { }; // 尚未执行的延迟操作 + service.syncPending(); // 下线:clearSession 应消费挂起状态 + check(fake.deactivate_count == 3, "syncPending 再次驱动下线"); + check(!fake.state.needs_refresh, "下线时 needs_refresh 被消费(不堵死后续 notify)"); + check(!fake.state.deferred_op, "下线时 deferred_op 随会话清理"); + spdlog::info("==== InteractionService self-check: {} ====", g_failures == 0 ? "ALL PASS" : "HAS FAILURES"); diff --git a/model/systems/core/InteractionState.h b/model/systems/core/InteractionState.h index 099804d..b023f3b 100644 --- a/model/systems/core/InteractionState.h +++ b/model/systems/core/InteractionState.h @@ -37,10 +37,13 @@ struct InteractionState { //!(覆盖语义:重复置位仅保留最新,操作须幂等) std::function deferred_op; - //! @brief 会话结束时的渲染层清理:清空标注(不动订阅,订阅随功能常驻) + //! @brief 会话结束时的渲染层清理:清空标注、消费挂起刷新与延迟操作(不动订阅,订阅随功能常驻) void clearSession() { annotations.clear(); + // 挂起状态随会话失效:残留 true 会堵死本功能后续 requestRefresh 的 notify(合并语义以"置位必有在途消费者"为前提) + needs_refresh = false; + deferred_op = nullptr; } }; -- Gitee From e9f53187239a813c0e6120b967358b3d2d64a602 Mon Sep 17 00:00:00 2001 From: a-ciue Date: Wed, 29 Jul 2026 16:12:46 +0800 Subject: [PATCH 13/13] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0AGENTS.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8ff1653..9bb5913 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ - `model/data/`:底层数据结构(`ModelData`、`MeshData`、`ModelManager` 等)。 - `model/ops/`:基于数据结构的操作,依赖 `model/data`。 - `model/systems/`:系统层(算法系统、模型 IO 系统、编辑系统、功能系统),负责插件注册与按字符串分发调用,依赖 `model/data`、`core`。 - - `model/systems/feature/`:功能系统 `FeatureSystem`,事件驱动的功能注册与调用:功能可注册参数/菜单/按键绑定,经 `EventBus` 订阅按键、参数变更、模型事件,通过 `FeatureContext` 访问模型层。 + - `model/systems/feature/`:功能系统 `FeatureSystem`,事件驱动的功能注册与调用:功能可注册参数/菜单/按键绑定,经 `EventBus` 订阅按键、参数变更、模型事件,通过 `FeatureContext` 访问模型层;声明 `interactive` 的功能另经 `InteractionContext` 订阅渲染线程驱动的视口交互(见第 10 节线程约定)。 - `app/`:程序与界面实现,依赖 `model`、`core`。 - `app/core/` → `core` - `app/model/` → `model`、`core`、`app/core`(model 的 Qt 接口、数据绑定) @@ -131,6 +131,7 @@ - 构建:`cmake --build out/build/x64-debug` - 测试:先以 `-DBUILD_TESTING=ON` 配置(默认 OFF),再 `ctest --test-dir out/build/x64-debug --output-on-failure` - 注意:`CMakeUserPresets.json` 含 `//` 注释,VS 的 CMake 集成可以容忍,但命令行 `cmake --preset` 会因解析失败而报错;命令行场景请手动传参(参照 preset 中的变量)或直接复用已配置好的构建目录。 + - 注意:命令行构建须先加载 MSVC 环境(`vcvars64.bat` 或 VS Developer PowerShell),否则报标准库头文件缺失(C1083 `fstream`/`array`);Git Bash 中调用 `cmd.exe` 需防路径转换(`MSYS_NO_PATHCONV=1`,`/c` 否则被转为 `C:/`)。 - 测试框架:模块单元测试用 **Catch2**(`cmake/test.cmake` 的 `precess_add_test` / `precess_test_link_libraries`),测试代码见各模块 `test/` 目录(如 `model/data/test/`、`model/systems/feature/test/`)。 - **新特性必须配套测试用例**;修 bug 时尽量补可复现的回归测试。 - 不要向无测试的模块强行塞测试框架;遵循该模块既有测试模式。 @@ -154,10 +155,13 @@ - 功能 `Handler` 封装进插件 `PluginHandler`,由 `SystemPluginManager` 注册到对应系统。 - 每类插件须实现对应系统接口完成数据交换;算法系统目前通过模型 IO 系统以文件读写交换模型数据。 - 每个插件目录含 `*.json` 描述文件(见 `plugins/*/.../*.json`)与 `CMakeLists.txt`;新增插件参照同目录既有示例结构。 -- 功能插件(`plugins/feature/`,json 的 `system` 字段为 `FeatureSystem`)实现 `FeatureHandler` 接口:在 `setup(FeatureRegistrar&)` 中注册参数、菜单项、按键绑定;在 `activate(FeatureContext&)` 中经 `ctx.events` 订阅事件(`KeyEvent`、`ParameterChangedEvent`、`ModelEvent`);菜单触发 `execute()`。功能可修改的范围仅限模型层对象(经 `FeatureContext` 的 `ModelLayer` / `ComponentOperator`),示例见 `plugins/feature/FeatureDemoPlugin/`。 +- 功能插件(`plugins/feature/`,json 的 `system` 字段为 `FeatureSystem`)实现 `FeatureHandler` 接口:在 `setup(FeatureRegistrar&)` 中注册参数、菜单项、按键绑定;在 `activate(FeatureContext&)` 中经 `ctx.events` 订阅事件(`KeyEvent`、`ParameterChangedEvent`、`ModelEvent`);菜单触发 `execute()`。功能可修改的范围限模型层对象(经 `FeatureContext` 的 `ModelLayer` / `ComponentOperator`)与自身视口交互状态(经 `ctx.interaction`);示例见 `plugins/feature/FeatureDemoPlugin/`,交互功能示例见 `plugins/feature/MeasurePlugin/`。 + - 订阅 `ParameterChangedEvent` **必须按 `e.feature` 过滤**本功能注册名(与 json 一致,参照 FeatureDemoPlugin 的 `kFeatureName` 常量),否则将响应其他功能的参数变更。 + - `Button` 类型参数为无值触发器:计数器载荷,功能约定忽略值、只读参数下标;点击经 `ParameterChangedEvent` 回到功能(GUI 线程)。 +- **视口交互线程约定**(声明 `interactive` 的功能,改动前先读 `InteractionState.h` 注释):交互回调(`onPick`/`onHover`/`onActivate`/`onDeactivate`)由 **渲染线程** 调用,`annotations` 为拉取契约(功能在回调中直写、渲染层拉取绘制);**GUI 线程不得直接修改交互状态与标注**,变更经 `requestRefresh()`(纯刷新通知,重复置位自动合并)或 `deferRefresh(op)`(操作延迟到渲染线程执行后再刷新)通知,渲染线程 `InteractionService::syncPending()` 统一消费;`setActive` 启停均自动 notify,单激活约定由 FeatureSystem 装配。 - **功能与界面解耦(声明 / 事件 / 上下文三原则)**:通用界面(`SideBar`、菜单、渲染窗口等)禁止按插件名 / 功能名特判。 - **静态能力走声明链**:视口交互能力(`interactive`)等一律经 json → `HandlerMetaData` → `FeatureInfo` → `QFeatureInfo` → QML 按声明渲染;新增交互功能不得改动通用界面代码。 - - **动态状态走事件回调**:交互结果、进度等经事件 / 信号传递(如 `interactionUpdated`、`EventBus` 事件),界面不轮询插件内部状态。 + - **动态状态走事件回调**:交互结果、进度等经事件 / 信号传递(如功能回写参数经 `ParameterChangedEvent` → `paramValueChanged` 信号同步 QML 显示),界面不轮询插件内部状态。 - **环境状态走上下文访问**:活动模型 / 组件、选择集经 `FeatureContext` provider 与 `App.selection` 获取,功能不反向依赖 app 层。 - 启停类逻辑做成幂等的状态应用(以目标状态为守卫,重复触发无副作用),避免多触发源的命令式调用堆积。 @@ -171,5 +175,6 @@ - [ ] 依赖方向正确,无循环/反向依赖;`core/` / `model/` / `cmake/` 未引入 AGPL/GPL 代码,`app/` / `plugins/` / `resource/` 新增依赖与 AGPLv3 兼容。 - [ ] 头文件按需前向声明 / include,无相对路径 include。 - [ ] 通用界面无插件名 / 功能名特判;插件静态能力、动态状态、环境状态分别经声明链、事件回调、上下文访问(第 10 节三原则)。 +- [ ] 视口交互状态仅由渲染线程修改;GUI 线程变更经 `requestRefresh` / `deferRefresh` 通知(第 10 节线程约定)。 - [ ] 能通过构建;涉及逻辑改动已(或建议)跑测试,新特性带测试。 - [ ] Commit 类型正确、单一职责、信息清晰。 \ No newline at end of file -- Gitee