#include "interact/InteractionManager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ColorLutBuilder.hpp" #include "interact/PickInteractorStyle.hpp" namespace geopro::render::interact { namespace { std::array imageBounds(vtkImageData* img) { std::array 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 跳过 Render(Qt 拆台时窗口可能已半析构) closeAll(); uninstallStyle(); } void InteractionManager::installStyle() { if (!interactor_ || style_) return; style_ = vtkSmartPointer::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(slices_.size())) return false; c = slices_[static_cast(selected_)]->center(); return true; }; interactor_->SetInteractorStyle(style_); // 右键菜单观察者:高优先级(1.0)直接挂交互器,先于 vtkImagePlaneWidget(默认 0.0)消费右键。 // 命中切片 → handleRightButton 内 abort + 弹菜单;未命中 → 不 abort,事件继续走默认。 rightBtnCmd_ = vtkSmartPointer::New(); rightBtnCmd_->SetClientData(this); rightBtnCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) { static_cast(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(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(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::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(axis); auto tool = std::make_unique(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(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(i)); selected_ = slices_.empty() ? -1 : std::min(selected_, static_cast(slices_.size()) - 1); updateSelectionVisual(); safeRender(); return; } } std::vector InteractionManager::shownSavedSliceIds() const { std::vector 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(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(i); break; } if (idx < 0) return; selected_ = idx; updateSelectionVisual(); // 双击切片正视(D40):同一切片在 350ms 内两次交互 → 视为双击 → 正视。 const double now = std::chrono::duration( 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(slices_.size())) return; const std::string closedDsId = slices_[static_cast(selected_)]->dsId(); slices_[static_cast(selected_)]->close(); slices_.erase(slices_.begin() + selected_); // 选中停在原位就近(删后该位变成下一张;删的是末张则退一张),不跳回 0(评审 M2)。 selected_ = slices_.empty() ? -1 : std::min(selected_, static_cast(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(slices_.size())) return false; const auto& s = slices_[static_cast(selected_)]; axis = static_cast(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(slices_.size())) return {}; return slices_[static_cast(selected_)]->dsId(); } void InteractionManager::tagSelectedSlice(const std::string& dsId) { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return; slices_[static_cast(selected_)]->setDsId(dsId); } vtkImageData* InteractionManager::selectedSliceImage() const { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; return slices_[static_cast(selected_)]->reslicedOutput(); } vtkSmartPointer 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(kExportLongSide) / longest : 1.0; if (f < 1.0) f = 1.0; // 不缩小(已够大则原样) vtkNew resize; resize->SetInputData(scalar); resize->SetResizeMethodToOutputDimensions(); resize->SetOutputDimensions(std::max(1, static_cast(nx * f)), std::max(1, static_cast(ny * f)), 1); resize->Update(); // 用与切片显示同一色阶 LUT 上色(colorScale_/vmin_/vmax_ 即当前体/切片着色区间)。 auto lut = buildLut(colorScale_, vmin_, vmax_); vtkNew map; map->SetInputConnection(resize->GetOutputPort()); map->SetLookupTable(lut); map->SetOutputFormatToRGB(); map->Update(); auto out = vtkSmartPointer::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 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(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 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 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(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(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(); // 保持当前观察距离 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(slices_.size())) return false; const double step = wheelStep(imageBounds(image_), dir); slices_[static_cast(selected_)]->advance(step); safeRender(); return true; // 消费滚轮(推进选中切片,不缩放) } } // namespace geopro::render::interact