456 lines
20 KiB
C++
456 lines
20 KiB
C++
#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 跳过 Render(Qt 拆台时窗口可能已半析构)
|
||
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
|