diff --git a/src/app/main.cpp b/src/app/main.cpp index 4b872d2..52ccc2d 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -543,6 +543,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* btnSliceFrontBack = new QPushButton(QStringLiteral("前后")); auto* btnSliceLeftRight = new QPushButton(QStringLiteral("左右")); auto* btnSliceOblique = new QPushButton(QStringLiteral("任意")); + auto* btnSliceFace = new QPushButton(QStringLiteral("正视")); // 正视选中切片(E54) auto* btnSliceFlip = new QPushButton(QStringLiteral("翻转")); auto* btnSliceClose = new QPushButton(QStringLiteral("关闭")); sliceLayout->addWidget(sliceLabel); @@ -550,6 +551,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re sliceLayout->addWidget(btnSliceFrontBack); sliceLayout->addWidget(btnSliceLeftRight); sliceLayout->addWidget(btnSliceOblique); + sliceLayout->addWidget(btnSliceFace); sliceLayout->addWidget(btnSliceFlip); sliceLayout->addWidget(btnSliceClose); sliceBar->setVisible(false); // 默认二维,不显示 @@ -557,13 +559,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 切片按钮可用性:仅三维 + 有体素时创建/翻转可用;关闭仅在有切片时可用。 auto updateSliceButtons = [interactionMgr, btnSliceUpDown, btnSliceFrontBack, btnSliceLeftRight, - btnSliceOblique, btnSliceFlip, btnSliceClose, sceneView]() { + btnSliceOblique, btnSliceFace, btnSliceFlip, btnSliceClose, sceneView]() { const bool canSlice = sceneView->hasVolume() && interactionMgr->hasVolume(); btnSliceUpDown->setEnabled(canSlice); btnSliceFrontBack->setEnabled(canSlice); btnSliceLeftRight->setEnabled(canSlice); btnSliceOblique->setEnabled(canSlice); btnSliceFlip->setEnabled(canSlice); + btnSliceFace->setEnabled(interactionMgr->hasSlices()); // 正视需有(选中)切片 btnSliceClose->setEnabled(interactionMgr->hasSlices()); }; updateSliceButtons(); @@ -581,6 +584,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re [addSlice]() { addSlice(SliceAxis::LeftRight); }); QObject::connect(btnSliceOblique, &QPushButton::clicked, vtkWidget, [addSlice]() { addSlice(SliceAxis::Oblique); }); + QObject::connect(btnSliceFace, &QPushButton::clicked, vtkWidget, + [interactionMgr]() { interactionMgr->faceSelected(); }); QObject::connect(btnSliceFlip, &QPushButton::clicked, vtkWidget, [interactionMgr]() { interactionMgr->flipView(); }); QObject::connect(btnSliceClose, &QPushButton::clicked, vtkWidget, diff --git a/src/render/interact/InteractionManager.cpp b/src/render/interact/InteractionManager.cpp index 8438cdb..e67fca1 100644 --- a/src/render/interact/InteractionManager.cpp +++ b/src/render/interact/InteractionManager.cpp @@ -77,12 +77,26 @@ void InteractionManager::setVolumeImage(vtkImageData* image, const geopro::core: void InteractionManager::addSlice(SliceAxis axis) { if (!image_ || !interactor_) return; auto tool = std::make_unique(image_, interactor_, axis, colorScale_, vmin_, vmax_); + // 触碰本切片(拖动/点击切面) → 设为选中(widget 开启交互后独占切面事件,选中靠此回调)。 + SliceTool* tp = tool.get(); + tool->onInteract = [this, tp]() { selectByTool(tp); }; slices_.push_back(std::move(tool)); selected_ = static_cast(slices_.size()) - 1; // 新切片选中 updateSelectionVisual(); safeRender(); } +void InteractionManager::selectByTool(const SliceTool* tool) { + for (std::size_t i = 0; i < slices_.size(); ++i) { + if (slices_[i].get() == tool) { + selected_ = static_cast(i); + updateSelectionVisual(); + safeRender(); + return; + } + } +} + void InteractionManager::closeSelected() { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return; slices_[static_cast(selected_)]->close(); @@ -143,12 +157,18 @@ void InteractionManager::onPicked(const Vec3& worldPoint) { } void InteractionManager::onDoubleClicked(const Vec3& worldPoint) { + // 双击命中切片 → 正视(widget 开启交互后双击多被其吞,正视主入口改工具条按钮 faceSelected)。 const int idx = nearestSlice(worldPoint); - if (idx < 0 || !renderer_) return; - auto* cam = renderer_->GetActiveCamera(); - if (!cam) return; + if (idx < 0) return; selected_ = idx; updateSelectionVisual(); + faceSlice(idx); +} + +void InteractionManager::faceSlice(int idx) { + if (idx < 0 || idx >= static_cast(slices_.size()) || !renderer_) return; + auto* cam = renderer_->GetActiveCamera(); + if (!cam) return; const Vec3 focal = slices_[static_cast(idx)]->center(); const Vec3 normal = slices_[static_cast(idx)]->normal(); const double dist = cam->GetDistance(); // 保持当前观察距离 @@ -161,6 +181,8 @@ void InteractionManager::onDoubleClicked(const Vec3& worldPoint) { safeRender(); } +void InteractionManager::faceSelected() { faceSlice(selected_); } + bool InteractionManager::onWheel(int dir) { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return false; const double step = wheelStep(imageBounds(image_), dir); diff --git a/src/render/interact/InteractionManager.hpp b/src/render/interact/InteractionManager.hpp index 6d75c5c..974196c 100644 --- a/src/render/interact/InteractionManager.hpp +++ b/src/render/interact/InteractionManager.hpp @@ -55,6 +55,10 @@ public: // 视图翻转:水平旋转 180°(E55)。 void flipView(); + // 正视选中切片(E54/D40):相机转到正对选中切面法向。无选中则忽略。 + // (改用工具条按钮触发,因 widget 开启交互后会抢双击事件。) + void faceSelected(); + // 安装/卸载自定义交互样式(构造时安装;析构卸载恢复原样式)。 void installStyle(); void uninstallStyle(); @@ -67,6 +71,10 @@ private: // 找离世界点最近的切片索引;无切片返回 -1。 int nearestSlice(const Vec3& worldPoint) const; + // 按 SliceTool 指针设为选中(widget 交互回调用:触碰即选中)。 + void selectByTool(const SliceTool* tool); + // 相机正视给定切面(focal=center, 沿 normal 退 dist)。 + void faceSlice(int idx); // 统一重绘:析构进行中(destroying_)跳过,避免 Qt 拆台时对半析构窗口 Render 崩溃(评审 H3)。 void safeRender(); diff --git a/src/render/interact/SliceTool.cpp b/src/render/interact/SliceTool.cpp index 1129924..39a6d94 100644 --- a/src/render/interact/SliceTool.cpp +++ b/src/render/interact/SliceTool.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include #include @@ -77,10 +79,15 @@ SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, } widget_->On(); - // 关闭 widget 自身的鼠标交互(窗位/光标/拖动):否则它会"吃掉"落在切片面上的左键, - // 自定义 PickInteractorStyle 收不到 → 单击选中/双击正视/绕点旋转全失效(实测根因)。 - // 关掉后切片仍正常显示,点击穿透到样式;切面移动改由滚轮(advance)驱动。 - widget_->InteractionOff(); + // 保持 widget 交互开启:任意切片可拖动调整角度/位置(F25 '可任意调整')。 + // 监听其交互开始事件 → 触碰本切片即回调 onInteract(上层据此设为选中)。 + interactObserver_ = vtkSmartPointer::New(); + interactObserver_->SetClientData(this); + interactObserver_->SetCallback([](vtkObject*, unsigned long, void* client, void*) { + auto* self = static_cast(client); + if (self && self->onInteract) self->onInteract(); + }); + widget_->AddObserver(vtkCommand::StartInteractionEvent, interactObserver_); } SliceTool::~SliceTool() { close(); } @@ -146,6 +153,11 @@ void SliceTool::setSelected(bool sel) { void SliceTool::close() { if (!widget_) return; + onInteract = nullptr; // 先断业务回调,避免 Off 期间触发到上层 + if (interactObserver_) { + widget_->RemoveObserver(interactObserver_); + interactObserver_ = nullptr; + } widget_->Off(); widget_->SetInteractor(nullptr); // 解除观察者,防悬挂崩溃 widget_ = nullptr; // 置空 → 二次 close()/析构真正幂等(不再 Off 已解绑 widget) diff --git a/src/render/interact/SliceTool.hpp b/src/render/interact/SliceTool.hpp index 93b1b81..98cabbe 100644 --- a/src/render/interact/SliceTool.hpp +++ b/src/render/interact/SliceTool.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include @@ -8,6 +9,7 @@ class vtkImageData; class vtkImagePlaneWidget; +class vtkCallbackCommand; class vtkRenderWindowInteractor; class vtkTrivialProducer; @@ -48,6 +50,10 @@ public: // 选中视觉反馈:选中→高亮边框(亮黄+粗线),未选中→暗淡细线。 void setSelected(bool sel); + // 用户开始操作本切片(拖动/点击切面)时回调 → 上层据此把本切片设为选中。 + // 因 widget 开启交互后独占切面鼠标事件,选中靠监听 widget 交互而非拾取。 + std::function onInteract; + // 世界点到本切面(无限平面)的垂直距离绝对值。供 picker 命中判定"点在哪张切片上"。 double distanceToPlane(const Vec3& worldPoint) const; @@ -60,6 +66,7 @@ private: // 把已存在的 image 接入 widget 的 producer:须随 SliceTool 保活(否则构造后析构→管线断裂崩溃,评审 H1)。 vtkSmartPointer producer_; vtkSmartPointer widget_; + vtkSmartPointer interactObserver_; // 监听 widget StartInteractionEvent → onInteract std::array imageBounds() const; };