#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_); } const InteractionManager::VolumeImg* InteractionManager::volumeOf(const std::string& volumeDsId) const { auto it = volumes_.find(volumeDsId); return it != volumes_.end() ? &it->second : nullptr; } vtkImageData* InteractionManager::selectedVolumeImage() const { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; const VolumeImg* v = volumeOf(slices_[static_cast(selected_)]->volumeDsId()); return v ? v->image : nullptr; } void InteractionManager::setVolumeImage(const std::string& volumeDsId, vtkImageData* image, const geopro::core::ColorScale& cs, double vmin, double vmax) { if (volumeDsId.empty()) return; auto it = volumes_.find(volumeDsId); // 同体 image 变更(重建/改色阶):旧 image 即将失效 → 先关该体已显示切片(上层 syncSlices 用新 image 重现)。 if (it != volumes_.end() && it->second.image != image) closeSlicesOfVolume(volumeDsId); volumes_[volumeDsId] = VolumeImg{image, cs, vmin, vmax}; } void InteractionManager::removeVolumeImage(const std::string& volumeDsId) { if (!volumes_.count(volumeDsId)) return; closeSlicesOfVolume(volumeDsId); // 体取消渲染 → 关其下所有切片 volumes_.erase(volumeDsId); } std::vector InteractionManager::volumeIds() const { std::vector ids; ids.reserve(volumes_.size()); for (const auto& kv : volumes_) ids.push_back(kv.first); return ids; } void InteractionManager::closeSlicesOfVolume(const std::string& volumeDsId) { for (std::size_t i = slices_.size(); i-- > 0;) { if (slices_[i]->volumeDsId() != volumeDsId) 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(); } void InteractionManager::addSlice(SliceAxis axis, const std::string& volumeDsId) { const VolumeImg* v = volumeOf(volumeDsId); if (!v || !v->image || !interactor_) return; auto tool = std::make_unique(v->image, interactor_, axis, v->cs, v->vmin, v->vmax); tool->setVolumeDsId(volumeDsId); // 触碰本切片(拖动/点击切面) → 设为选中(widget 开启交互后独占切面事件,选中靠此回调)。 SliceTool* tp = tool.get(); tool->onInteract = [this, tp]() { selectByTool(tp); }; slices_.push_back(std::move(tool)); selected_ = static_cast(slices_.size()) - 1; // 新切片选中 updateSelectionVisual(); if (onSliceSelectionChanged) onSliceSelectionChanged(std::string{}); // 新建(未保存)切片→清列表选中 safeRender(); } void InteractionManager::showSavedSlice(const std::string& dsId, int axis, const Vec3& origin, const Vec3& point1, const Vec3& point2, const std::string& volumeDsId) { const VolumeImg* v = volumeOf(volumeDsId); if (!v || !v->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(v->image, interactor_, ax, v->cs, v->vmin, v->vmax, origin, point1, point2); // 三点精确还原 tool->setDsId(dsId); tool->setVolumeDsId(volumeDsId); SliceTool* tp = tool.get(); tool->onInteract = [this, tp]() { selectByTool(tp); }; tool->setInteractive(false); // 已保存切片定稿锁定:不可移动/旋转(用户要求);仍可拾取选中/右键 slices_.push_back(std::move(tool)); selected_ = static_cast(slices_.size()) - 1; updateSelectionVisual(); // 程序化显示(syncSlices):不发 onSliceSelectionChanged,避免列表选中被刷 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::deselectSlice() { if (selected_ < 0) return; selected_ = -1; updateSelectionVisual(); // 清高亮(无选中切片) safeRender(); } 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(); if (onSliceSelectionChanged) // 反向 VTK→list:选中切片 → 列表同步选中(dsId 空=临时切片) onSliceSelectionChanged(slices_[static_cast(idx)]->dsId()); // 双击切片正视(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(); } PickInteractorStyle* InteractionManager::pickStyle() const { return style_; } void InteractionManager::setMode2D(bool is2D) { // 进入二维分析:主动取消「三维前视图」的所有选中。否则残留的选中切片会让 onWheel 持续消费滚轮 // (二维下无法缩放),且切回三维仍残留高亮。清 selected_ + 切片高亮;再经 onSliceSelectionChanged("") // 联动清三维分析列表选中行与异常高亮(app 层接线)。与 VtkSceneView::setAnalysisMode2D 离开二维时 // clearMapLineSelection 清足迹选中相对称。 if (is2D) { if (selected_ >= 0) { selected_ = -1; updateSelectionVisual(); // 清切片高亮(切回三维不残留选中) } if (onSliceSelectionChanged) onSliceSelectionChanged(std::string{}); } // 切片属三维内容:二维分析隐藏(不销毁→切回零重建)、三维分析显示。 for (auto& s : slices_) if (s) s->setVisible(!is2D); if (style_) style_->setLock2D(is2D); // 二维=禁旋转、左键平移(仅平移+缩放) // 不在此渲染:相机/地形/底图/维度显隐及统一 Render 由 VtkSceneView::setAnalysisMode2D 收尾。 } 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(); } std::string InteractionManager::selectedSliceVolumeDsId() const { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return {}; return slices_[static_cast(selected_)]->volumeDsId(); } void InteractionManager::tagSelectedSlice(const std::string& dsId) { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return; slices_[static_cast(selected_)]->setDsId(dsId); slices_[static_cast(selected_)]->setInteractive(false); // 保存即定稿锁定(不可改) } vtkImageData* InteractionManager::selectedSliceImage() const { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; return slices_[static_cast(selected_)]->reslicedOutput(); } vtkSmartPointer InteractionManager::selectedSliceColorImage() const { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; // 与屏幕切片**同源**的着色输出(widget 自己的 ColorMap 输出, 逐像素一致, RGBA 外区透明)。 // 原先另建 LUT 上色, 与屏幕配色可能不一致(用户实测异常截图与切面差异大) → 改取 widget 着色结果。 auto colored = slices_[static_cast(selected_)]->coloredResliceImage(); if (colored == nullptr) return nullptr; // 高清化:切片重采样像素维度受体素分辨率限制(常仅几十px) → 上采样到目标分辨率(双线性, 与屏幕 // TextureInterpolateOn 同口径), 得清晰大图。对 RGBA 直接插值(色已定, 不再过 LUT)。 constexpr int kExportLongSide = 2048; int dims[3]; colored->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(colored); resize->SetResizeMethodToOutputDimensions(); resize->SetOutputDimensions(std::max(1, static_cast(nx * f)), std::max(1, static_cast(ny * f)), 1); resize->Update(); auto out = vtkSmartPointer::New(); out->DeepCopy(resize->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 VolumeImg* vol = volumeOf(slices_[static_cast(idx)]->volumeDsId()); const std::array b = imageBounds(vol ? vol->image : nullptr); 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(); if (onSliceSelectionChanged) // 反向 VTK→list:点中切片→列表同步选中;点空(idx<0)→清列表选中 onSliceSelectionChanged(selected_ >= 0 ? slices_[static_cast(selected_)]->dsId() : std::string{}); 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(selectedVolumeImage()), dir); // 选中切片所属体 slices_[static_cast(selected_)]->advance(step); safeRender(); return true; // 消费滚轮(推进选中切片,不缩放) } } // namespace geopro::render::interact