fix(vtk): 恢复任意切片可调(F25)+触碰选中+正视按钮(不再砍功能)

之前用 InteractionOff 砍掉了切片可调(F25),错。改回:
- widget 交互保持开启 → 任意切片可拖动调整角度/位置(F25 恢复); 拖切面=widget 处理。
- 选中改为监听 widget StartInteractionEvent(SliceTool::onInteract → selectByTool):
  触碰某切片即选中+高亮(widget 开交互后独占切面事件,拾取式选中失效,故改观察式)。
- 正视(E54)从双击改为工具条「正视」按钮(faceSelected): 双击会被 widget 抢、QVTK 上本不稳。
- 滚轮推进/关闭 仍作用于选中切片; 相机旋转=空白处拖动(默认 trackball)。
- ctest 221/221
This commit is contained in:
gaozheng 2026-06-16 10:50:05 +08:00
parent 43f8228e49
commit 87c5cc910e
5 changed files with 62 additions and 8 deletions

View File

@ -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,

View File

@ -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<SliceTool>(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<int>(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<int>(i);
updateSelectionVisual();
safeRender();
return;
}
}
}
void InteractionManager::closeSelected() {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
slices_[static_cast<std::size_t>(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<int>(slices_.size()) || !renderer_) return;
auto* cam = renderer_->GetActiveCamera();
if (!cam) return;
const Vec3 focal = slices_[static_cast<std::size_t>(idx)]->center();
const Vec3 normal = slices_[static_cast<std::size_t>(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<int>(slices_.size())) return false;
const double step = wheelStep(imageBounds(image_), dir);

View File

@ -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();

View File

@ -3,6 +3,8 @@
#include <algorithm>
#include <cmath>
#include <vtkCallbackCommand.h>
#include <vtkCommand.h>
#include <vtkImageData.h>
#include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h>
@ -77,10 +79,15 @@ SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor,
}
widget_->On();
// 关闭 widget 自身的鼠标交互(窗位/光标/拖动):否则它会"吃掉"落在切片面上的左键,
// 自定义 PickInteractorStyle 收不到 → 单击选中/双击正视/绕点旋转全失效(实测根因)。
// 关掉后切片仍正常显示,点击穿透到样式;切面移动改由滚轮(advance)驱动。
widget_->InteractionOff();
// 保持 widget 交互开启:任意切片可拖动调整角度/位置(F25 '可任意调整')。
// 监听其交互开始事件 → 触碰本切片即回调 onInteract(上层据此设为选中)。
interactObserver_ = vtkSmartPointer<vtkCallbackCommand>::New();
interactObserver_->SetClientData(this);
interactObserver_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
auto* self = static_cast<SliceTool*>(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

View File

@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <functional>
#include <vtkSmartPointer.h>
@ -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<void()> onInteract;
// 世界点到本切面(无限平面)的垂直距离绝对值。供 picker 命中判定"点在哪张切片上"。
double distanceToPlane(const Vec3& worldPoint) const;
@ -60,6 +66,7 @@ private:
// 把已存在的 image 接入 widget 的 producer须随 SliceTool 保活(否则构造后析构→管线断裂崩溃,评审 H1
vtkSmartPointer<vtkTrivialProducer> producer_;
vtkSmartPointer<vtkImagePlaneWidget> widget_;
vtkSmartPointer<vtkCallbackCommand> interactObserver_; // 监听 widget StartInteractionEvent → onInteract
std::array<double, 6> imageBounds() const;
};