geopro/src/render/interact/InteractionManager.cpp

378 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "interact/InteractionManager.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCellPicker.h>
#include <vtkCommand.h>
#include <vtkImageData.h>
#include <vtkImageMapToColors.h>
#include <vtkImageResize.h>
#include <vtkLookupTable.h>
#include <vtkNew.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include "ColorLutBuilder.hpp"
#include "interact/PickInteractorStyle.hpp"
namespace geopro::render::interact {
namespace {
std::array<double, 6> imageBounds(vtkImageData* img) {
std::array<double, 6> b{{0, 0, 0, 0, 0, 0}};
if (img) img->GetBounds(b.data());
return b;
}
} // namespace
InteractionManager::InteractionManager(vtkRenderWindowInteractor* interactor,
vtkRenderWindow* renderWindow, vtkRenderer* renderer)
: interactor_(interactor), renderWindow_(renderWindow), renderer_(renderer) {
installStyle();
}
InteractionManager::~InteractionManager() {
destroying_ = true; // closeAll 跳过 RenderQt 拆台时窗口可能已半析构)
closeAll();
uninstallStyle();
}
void InteractionManager::installStyle() {
if (!interactor_ || style_) return;
style_ = vtkSmartPointer<PickInteractorStyle>::New();
style_->onPick = [this](const Vec3& w) { onPicked(w); };
style_->onDoubleClick = [this](const Vec3& w) { onDoubleClicked(w); };
style_->onWheelStep = [this](int dir) { return onWheel(dir); };
// D39: 提供旋转中心 = 选中切片中心有选中→true。style 在按下拖动时据此绕选中切片旋转。
style_->getRotateCenter = [this](Vec3& c) {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
c = slices_[static_cast<std::size_t>(selected_)]->center();
return true;
};
interactor_->SetInteractorStyle(style_);
// 右键菜单观察者:高优先级(1.0)直接挂交互器,先于 vtkImagePlaneWidget(默认 0.0)消费右键。
// 命中切片 → handleRightButton 内 abort + 弹菜单;未命中 → 不 abort事件继续走默认。
rightBtnCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
rightBtnCmd_->SetClientData(this);
rightBtnCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<InteractionManager*>(client)->handleRightButton();
});
rightBtnTag_ = interactor_->AddObserver(vtkCommand::RightButtonPressEvent, rightBtnCmd_, 1.0);
}
void InteractionManager::uninstallStyle() {
if (style_) {
// 断开回调this 即将析构),避免迟到事件回调悬垂。
style_->onPick = nullptr;
style_->onDoubleClick = nullptr;
style_->onWheelStep = nullptr;
style_->getRotateCenter = nullptr;
}
// 摘除右键观察者this 即将析构)。
if (interactor_ && rightBtnTag_ != 0) {
interactor_->RemoveObserver(rightBtnTag_);
rightBtnTag_ = 0;
}
rightBtnCmd_ = nullptr;
// 从 interactor 上彻底摘除自定义 style避免 interactor 仍持空回调 style评审 H2
if (interactor_) interactor_->SetInteractorStyle(nullptr);
style_ = nullptr;
}
void InteractionManager::safeRender() {
if (renderWindow_ && !destroying_) renderWindow_->Render();
}
void InteractionManager::updateSelectionVisual() {
for (std::size_t i = 0; i < slices_.size(); ++i)
slices_[i]->setSelected(static_cast<int>(i) == selected_);
}
void InteractionManager::setVolumeImage(vtkImageData* image, const geopro::core::ColorScale& cs,
double vmin, double vmax) {
// 体素重建/变更:先释放旧切片(旧 image 即将失效),再附着新 image。
closeAll();
image_ = image;
colorScale_ = cs;
vmin_ = vmin;
vmax_ = vmax;
}
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::showSavedSlice(const std::string& dsId, int axis, const Vec3& origin,
const Vec3& point1, const Vec3& point2) {
if (!image_ || !interactor_ || dsId.empty()) return;
for (const auto& s : slices_)
if (s->dsId() == dsId) return; // 已显示 → 去重跳过
const SliceAxis ax = static_cast<SliceAxis>(axis);
auto tool = std::make_unique<SliceTool>(image_, interactor_, ax, colorScale_, vmin_, vmax_,
origin, point1, point2); // 三点精确还原
tool->setDsId(dsId);
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::hideSavedSlice(const std::string& dsId) {
for (std::size_t i = 0; i < slices_.size(); ++i) {
if (slices_[i]->dsId() != dsId) continue;
slices_[i]->close();
slices_.erase(slices_.begin() + static_cast<long>(i));
selected_ = slices_.empty() ? -1
: std::min(selected_, static_cast<int>(slices_.size()) - 1);
updateSelectionVisual();
safeRender();
return;
}
}
std::vector<std::string> InteractionManager::shownSavedSliceIds() const {
std::vector<std::string> out;
for (const auto& s : slices_)
if (!s->dsId().empty()) out.push_back(s->dsId());
return out;
}
bool InteractionManager::selectSavedSlice(const std::string& dsId) {
for (std::size_t i = 0; i < slices_.size(); ++i) {
if (slices_[i]->dsId() != dsId) continue;
selected_ = static_cast<int>(i);
updateSelectionVisual();
safeRender();
return true;
}
return false;
}
void InteractionManager::selectByTool(const SliceTool* tool) {
int idx = -1;
for (std::size_t i = 0; i < slices_.size(); ++i)
if (slices_[i].get() == tool) { idx = static_cast<int>(i); break; }
if (idx < 0) return;
selected_ = idx;
updateSelectionVisual();
// 双击切片正视(D40):同一切片在 350ms 内两次交互 → 视为双击 → 正视。
const double now = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
const bool dbl = (tool == lastInteractTool_) && lastInteractMs_ >= 0.0 &&
(now - lastInteractMs_) < 350.0;
lastInteractMs_ = now;
lastInteractTool_ = tool;
if (dbl) {
lastInteractMs_ = -1.0; // 重置避免三连判
faceSlice(idx);
return;
}
safeRender();
}
void InteractionManager::closeSelected() {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
const std::string closedDsId = slices_[static_cast<std::size_t>(selected_)]->dsId();
slices_[static_cast<std::size_t>(selected_)]->close();
slices_.erase(slices_.begin() + selected_);
// 选中停在原位就近(删后该位变成下一张;删的是末张则退一张),不跳回 0评审 M2
selected_ = slices_.empty() ? -1
: std::min(selected_, static_cast<int>(slices_.size()) - 1);
updateSelectionVisual();
safeRender();
// 已保存切片被主动关闭 → 通知上层取消列表勾选(场景↔列表同步)。
if (!closedDsId.empty() && onSliceClosed) onSliceClosed(closedDsId);
}
void InteractionManager::closeAll() {
for (auto& s : slices_) s->close(); // 显式 Off + 解绑(析构亦会,双保险幂等)
slices_.clear();
selected_ = -1;
safeRender();
}
void InteractionManager::flipView() {
if (!renderer_) return;
auto* cam = renderer_->GetActiveCamera();
if (!cam) return;
cam->Azimuth(180.0); // 水平旋转 180°E55
cam->OrthogonalizeViewUp();
safeRender();
}
void InteractionManager::faceSelected() { faceSlice(selected_); }
bool InteractionManager::selectedSlicePlane(int& axis, Vec3& origin, Vec3& point1,
Vec3& point2) const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
const auto& s = slices_[static_cast<std::size_t>(selected_)];
axis = static_cast<int>(s->axis());
double o[3], p1[3], p2[3];
s->planePoints(o, p1, p2);
origin = {{o[0], o[1], o[2]}};
point1 = {{p1[0], p1[1], p1[2]}};
point2 = {{p2[0], p2[1], p2[2]}};
return true;
}
std::string InteractionManager::selectedSliceDsId() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return {};
return slices_[static_cast<std::size_t>(selected_)]->dsId();
}
void InteractionManager::tagSelectedSlice(const std::string& dsId) {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
slices_[static_cast<std::size_t>(selected_)]->setDsId(dsId);
}
vtkImageData* InteractionManager::selectedSliceImage() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
return slices_[static_cast<std::size_t>(selected_)]->reslicedOutput();
}
vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() const {
vtkImageData* scalar = selectedSliceImage();
if (scalar == nullptr) return nullptr;
// 高清导出切片重采样像素维度受体素网格分辨率限制常仅几十px→ 先上采样到目标分辨率
// (最长边 kExportLongSide保持长宽比、插值再上色得到清晰大图。
constexpr int kExportLongSide = 2048;
int dims[3];
scalar->GetDimensions(dims);
const int nx = dims[0], ny = dims[1];
const int longest = std::max(nx, ny);
double f = (longest > 0) ? static_cast<double>(kExportLongSide) / longest : 1.0;
if (f < 1.0) f = 1.0; // 不缩小(已够大则原样)
vtkNew<vtkImageResize> resize;
resize->SetInputData(scalar);
resize->SetResizeMethodToOutputDimensions();
resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)),
std::max(1, static_cast<int>(ny * f)), 1);
resize->Update();
// 用与切片显示同一色阶 LUT 上色colorScale_/vmin_/vmax_ 即当前体/切片着色区间)。
auto lut = buildLut(colorScale_, vmin_, vmax_);
vtkNew<vtkImageMapToColors> map;
map->SetInputConnection(resize->GetOutputPort());
map->SetLookupTable(lut);
map->SetOutputFormatToRGB();
map->Update();
auto out = vtkSmartPointer<vtkImageData>::New();
out->DeepCopy(map->GetOutput()); // 深拷贝脱离 filter 生命周期
return out;
}
int InteractionManager::pickSliceAtCursor() const {
if (!interactor_ || slices_.empty()) return -1;
const int* pos = interactor_->GetEventPosition();
auto* ren = interactor_->FindPokedRenderer(pos[0], pos[1]);
if (!ren) return -1;
vtkNew<vtkCellPicker> picker;
picker->SetTolerance(0.005);
if (!picker->Pick(pos[0], pos[1], 0.0, ren)) return -1;
double w[3];
picker->GetPickPosition(w);
return nearestSlice({w[0], w[1], w[2]});
}
void InteractionManager::handleRightButton() {
// 高优先级右键观察者(先于 vtkImagePlaneWidget 消费右键)。
// 选中目标 = 拾取命中的切片;拾取没命中(常因拾到体/其它面)则回退到"当前选中切片"。
// 有可操作切片 → abort 右键 + 弹菜单;否则放行默认右键。
if (!interactor_) return;
int idx = pickSliceAtCursor();
if (idx < 0) idx = selected_; // 回退到当前选中切片
if (idx < 0 || idx >= static_cast<int>(slices_.size())) return; // 无切片可操作 → 放行默认右键
selected_ = idx;
updateSelectionVisual();
safeRender();
if (rightBtnCmd_) rightBtnCmd_->SetAbortFlag(1); // 消费右键,阻止 widget/style 默认行为
if (onSliceContextMenuRequested) onSliceContextMenuRequested();
}
int InteractionManager::nearestSlice(const Vec3& worldPoint) const {
if (slices_.empty()) return -1;
std::vector<Vec3> centers, normals;
centers.reserve(slices_.size());
normals.reserve(slices_.size());
for (const auto& s : slices_) {
centers.push_back(s->center());
normals.push_back(s->normal());
}
const int idx = nearestPlane(centers, normals, worldPoint);
if (idx < 0) return -1;
// 阈值:命中点离最近切面太远(> 体对角线 5%)视为"没点在切片上",不改选中(评审 M2
const std::array<double, 6> b = imageBounds(image_);
const double dx = b[1] - b[0], dy = b[3] - b[2], dz = b[5] - b[4];
const double diag = std::sqrt(dx * dx + dy * dy + dz * dz);
const double dist = slices_[static_cast<std::size_t>(idx)]->distanceToPlane(worldPoint);
if (diag > 0.0 && dist > diag * 0.05) return -1;
return idx;
}
void InteractionManager::onPicked(const Vec3& worldPoint) {
// 单击 = 选中命中切片;点在切片外(如点到体/帘面)→ 取消选中idx=-1。**不动相机**。
// 解决"选了切片无法取消":点击切片之外即清选中,滚轮恢复缩放(见 onWheel
selected_ = nearestSlice(worldPoint);
updateSelectionVisual();
safeRender();
}
void InteractionManager::onDoubleClicked(const Vec3& worldPoint) {
// 双击命中切片 → 正视widget 开启交互后双击多被其吞,正视主入口改工具条按钮 faceSelected
const int idx = nearestSlice(worldPoint);
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(); // 保持当前观察距离
const FaceOnCamera face = faceOnCamera(focal, normal, dist);
cam->SetFocalPoint(focal[0], focal[1], focal[2]);
cam->SetPosition(face.position[0], face.position[1], face.position[2]);
cam->SetViewUp(face.viewUp[0], face.viewUp[1], face.viewUp[2]);
cam->OrthogonalizeViewUp();
renderer_->ResetCameraClippingRange();
safeRender();
}
bool InteractionManager::onWheel(int dir) {
// 滚轮推进**当前选中**的切片(需先显式选中);无选中 → 不消费 → 相机缩放。
// 配合 onPicked 的"点击切片外取消选中":取消后滚轮即恢复缩放,解决"选了切片无法缩放"。
// (不采用"悬停即推进":推进时鼠标难持续压在移动的切片上,且过敏感。)
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
const double step = wheelStep(imageBounds(image_), dir);
slices_[static_cast<std::size_t>(selected_)]->advance(step);
safeRender();
return true; // 消费滚轮(推进选中切片,不缩放)
}
} // namespace geopro::render::interact