geopro/src/render/interact/PickInteractorStyle.cpp

181 lines
7.6 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/PickInteractorStyle.hpp"
#include <chrono>
#include <cstring>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCellPicker.h>
#include <vtkMath.h>
#include <vtkNew.h>
#include <vtkObjectFactory.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkTransform.h>
namespace geopro::render::interact {
namespace {
constexpr double kDoubleClickMs = 350.0; // 两次左键按下间隔阈值
constexpr int kClickSlopPx2 = 36; // 位置相近阈值平方(6px)
// 当前单调时钟(毫秒)。用 std::chrono 避免依赖 VTK::CommonSystem(vtkTimerLog)。
double nowMs() {
return std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
} // namespace
vtkStandardNewMacro(PickInteractorStyle);
bool PickInteractorStyle::pickWorld(Vec3& out) {
auto* iren = this->GetInteractor();
if (!iren) return false;
const int* pos = iren->GetEventPosition();
// 用交互器解析被点中的 renderer基类 FindPokedRenderer 仅设 CurrentRenderer、返回 void
auto* ren = iren->FindPokedRenderer(pos[0], pos[1]);
if (!ren) return false;
// CellPicker返回表面交点世界坐标命中切片纹理面/帘面等)。
vtkNew<vtkCellPicker> picker;
picker->SetTolerance(0.005);
if (!picker->Pick(pos[0], pos[1], 0.0, ren)) return false;
double w[3];
picker->GetPickPosition(w);
out = {w[0], w[1], w[2]};
return true;
}
void PickInteractorStyle::OnLeftButtonDown() {
auto* iren = this->GetInteractor();
Vec3 world;
const bool hit = pickWorld(world); // 仍用于取选中所需世界点(onPick)
// 命中切片【精确判定】:光标射线穿过某切片真实矩形内才算(不靠带容差的 picker 点)。
// 边界外/帘面/其它物 → false → 抬键单击即取消。
downHitSlice_ = hitTestSlice && hitTestSlice();
// 手动双击判定GetRepeatCount 在 QVTK+Windows 不可靠,评审 M5
// 两次左键按下间隔 < 阈值且屏幕位置相近 → 双击。
const double now = nowMs();
const int* pos = iren ? iren->GetEventPosition() : nullptr;
bool isDouble = false;
if (downHitSlice_ && pos && lastDownTime_ >= 0.0) { // 仅命中切片才判双击(正视)
const double dtMs = now - lastDownTime_;
const int dx = pos[0] - lastDownPos_[0];
const int dy = pos[1] - lastDownPos_[1];
if (dtMs < kDoubleClickMs && (dx * dx + dy * dy) <= kClickSlopPx2) isDouble = true;
}
if (pos) {
lastDownPos_[0] = pos[0];
lastDownPos_[1] = pos[1];
}
lastDownTime_ = now;
if (isDouble) {
// 双击命中 → 正视所在切片manager 找最近切片 + 算相机)。
if (onDoubleClick) onDoubleClick(world);
lastDownTime_ = -1.0; // 重置,避免三击连判
return; // 不进入拖动旋转
}
if (downHitSlice_ && hit) {
// 单击命中【切片】→ 选中onPick 内仅选中, 不动相机)。点帘面/非切片物/边界外不选中→抬键即取消。
if (onPick) onPick(world);
}
// 捕获本次拖动的旋转支点【一次】(在 onPick 选中之后取,故能用新选中切片):拖动中光标会移动,
// 不能每帧重取(否则支点漂)。选中切片→其中心;否则→光标射线穿过的体中段点;无→默认绕焦点。
hasRotatePivot_ = (getRotateCenter && getRotateCenter(rotatePivot_));
// 不在按下时动相机(动相机=跳);绕支点旋转在 Rotate() 内做(增量绕支点,不跳)。
Superclass::OnLeftButtonDown();
}
void PickInteractorStyle::Rotate() {
if (!this->CurrentRenderer || !hasRotatePivot_) {
Superclass::Rotate(); // 无支点 → 默认绕焦点旋转
return;
}
const Vec3 c = rotatePivot_; // 按下时已捕获、拖动中固定
auto* rwi = this->Interactor;
auto* cam = this->CurrentRenderer->GetActiveCamera();
if (!rwi || !cam) return;
const int dx = rwi->GetEventPosition()[0] - rwi->GetLastEventPosition()[0];
const int dy = rwi->GetEventPosition()[1] - rwi->GetLastEventPosition()[1];
const int* size = this->CurrentRenderer->GetRenderWindow()->GetSize();
if (size[0] <= 0 || size[1] <= 0) return;
// 与 TrackballCamera 同口径的角度映射。
// 俯仰:本类绕 right=cross(DOP,up) 旋转,而 VTK 默认 Elevation 绕 cross(-DOP,up)=-right →
// 轴反向。故 elevation 取 +20非 -20抵消使选中切片后上下方向与未选中(默认)时一致。
const double azimuth = dx * (-20.0 / size[0]) * this->MotionFactor;
const double elevation = dy * (20.0 / size[1]) * this->MotionFactor;
double up[3], dop[3], right[3];
cam->GetViewUp(up);
cam->GetDirectionOfProjection(dop); // 归一化的 (focal-pos)
vtkMath::Cross(dop, up, right); // 屏幕"右"轴
vtkMath::Normalize(right);
// 绕中心 c 的支点T(c)·R(up,azimuth)·R(right,elevation)·T(-c),作用于 position/focalup 只转不平移。
vtkNew<vtkTransform> t;
t->Identity();
t->Translate(c[0], c[1], c[2]);
t->RotateWXYZ(azimuth, up[0], up[1], up[2]);
t->RotateWXYZ(elevation, right[0], right[1], right[2]);
t->Translate(-c[0], -c[1], -c[2]);
double pos[3], foc[3], npos[3], nfoc[3], nup[3];
cam->GetPosition(pos);
cam->GetFocalPoint(foc);
t->TransformPoint(pos, npos);
t->TransformPoint(foc, nfoc);
t->TransformVector(up, nup); // 仅旋转部分作用于向量
cam->SetPosition(npos);
cam->SetFocalPoint(nfoc);
cam->SetViewUp(nup);
cam->OrthogonalizeViewUp();
if (this->AutoAdjustCameraClippingRange) this->CurrentRenderer->ResetCameraClippingRange();
rwi->Render();
}
void PickInteractorStyle::OnMouseMove() {
Superclass::OnMouseMove();
}
void PickInteractorStyle::OnLeftButtonUp() {
// 单击(抬键位移<阈值=非拖动)且按下未命中切片 → 取消选中(点空/点体;体 PickableOff 故点体也 hit=false
// 拖空白旋转:抬键位移大 → 不取消,保留"绕选中切片旋转"。Esc 仍是完全拉近时的兜底。
if (!downHitSlice_ && onDeselect) {
auto* iren = this->GetInteractor();
const int* up = iren ? iren->GetEventPosition() : nullptr;
if (up) {
const int dx = up[0] - lastDownPos_[0], dy = up[1] - lastDownPos_[1];
if (dx * dx + dy * dy <= kClickSlopPx2) onDeselect(); // 是单击 → 取消
}
}
hasRotatePivot_ = false; // 拖动结束 → 清支点(下次按下重新捕获)
Superclass::OnLeftButtonUp(); // 平移/旋转/缩放等由基类按 State 收尾
}
void PickInteractorStyle::OnMouseWheelForward() {
// 有选中切片 → 沿法向推进(消费滚轮);否则默认缩放。
if (onWheelStep && onWheelStep(+1)) return; // 有选中切片 → 推进,消费滚轮
Superclass::OnMouseWheelForward(); // 否则默认缩放
}
void PickInteractorStyle::OnMouseWheelBackward() {
if (onWheelStep && onWheelStep(-1)) return;
Superclass::OnMouseWheelBackward();
}
void PickInteractorStyle::OnKeyPress() {
// Esc → 取消选中切片(拉近后满屏切片、点不到空白处取消时的可靠出口)。其它键走默认。
auto* iren = this->GetInteractor();
const char* sym = iren ? iren->GetKeySym() : nullptr;
if (sym && std::strcmp(sym, "Escape") == 0) {
if (onDeselect) onDeselect();
return;
}
Superclass::OnKeyPress();
}
} // namespace geopro::render::interact