geopro/src/render/interact/InteractionManager.cpp

456 lines
20 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_);
}
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<int>(slices_.size())) return nullptr;
const VolumeImg* v = volumeOf(slices_[static_cast<std::size_t>(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<std::string> InteractionManager::volumeIds() const {
std::vector<std::string> 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<long>(i));
}
selected_ = slices_.empty() ? -1 : std::min(selected_, static_cast<int>(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<SliceTool>(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<int>(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<SliceAxis>(axis);
auto tool = std::make_unique<SliceTool>(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<int>(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<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::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<int>(i); break; }
if (idx < 0) return;
selected_ = idx;
updateSelectionVisual();
if (onSliceSelectionChanged) // 反向 VTK→list选中切片 → 列表同步选中dsId 空=临时切片)
onSliceSelectionChanged(slices_[static_cast<std::size_t>(idx)]->dsId());
// 双击切片正视(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();
}
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<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();
}
std::string InteractionManager::selectedSliceVolumeDsId() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return {};
return slices_[static_cast<std::size_t>(selected_)]->volumeDsId();
}
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);
slices_[static_cast<std::size_t>(selected_)]->setInteractive(false); // 保存即定稿锁定(不可改)
}
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 {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
// 与屏幕切片**同源**的着色输出(widget 自己的 ColorMap 输出, 逐像素一致, RGBA 外区透明)。
// 原先另建 LUT 上色, 与屏幕配色可能不一致(用户实测异常截图与切面差异大) → 改取 widget 着色结果。
auto colored = slices_[static_cast<std::size_t>(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<double>(kExportLongSide) / longest : 1.0;
if (f < 1.0) f = 1.0; // 不缩小
vtkNew<vtkImageResize> resize;
resize->SetInputData(colored);
resize->SetResizeMethodToOutputDimensions();
resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)),
std::max(1, static_cast<int>(ny * f)), 1);
resize->Update();
auto out = vtkSmartPointer<vtkImageData>::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<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 VolumeImg* vol = volumeOf(slices_[static_cast<std::size_t>(idx)]->volumeDsId());
const std::array<double, 6> 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<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();
if (onSliceSelectionChanged) // 反向 VTK→list点中切片→列表同步选中点空(idx<0)→清列表选中
onSliceSelectionChanged(selected_ >= 0
? slices_[static_cast<std::size_t>(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<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(selectedVolumeImage()), dir); // 选中切片所属体
slices_[static_cast<std::size_t>(selected_)]->advance(step);
safeRender();
return true; // 消费滚轮(推进选中切片,不缩放)
}
} // namespace geopro::render::interact