feat(vtk): 导航gizmo改业界轴球风(平涂盘+白字标签+负轴淡环+hover高亮+抗锯齿)

- 方向球 LightingOff 平涂实心盘(theta/phi=48),非高光3D球
- 柔和调色板 X(0.90,0.30,0.36)/Y(0.55,0.78,0.33)/Z(0.28,0.45,0.90)
- 正向球稍大+白色XYZ公告板字标(推到球前避遮挡),负向球同色×0.42更暗更小无字标(仍可拾取)
- 细轴线(1.8)连正向球、同色、不可拾取
- 新增 MouseMove hover 观察者(不abort):角落内拾取→提亮+就地放大1.18×,离开复原
- 整窗 MSAA(8x,有守卫) + 视口上抬避开沿线条 + parallelScale 按视口长宽比自适应不裁切
- CMake 增补 RenderingFreeType 供白字字形运行时渲染
This commit is contained in:
gaozheng 2026-07-01 13:52:03 +08:00
parent 8b32566351
commit 48b8e582ef
3 changed files with 155 additions and 36 deletions

View File

@ -37,6 +37,7 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)
find_package(VTK REQUIRED COMPONENTS find_package(VTK REQUIRED COMPONENTS
GUISupportQt GUISupportQt
RenderingOpenGL2 RenderingOpenGL2
RenderingFreeType # gizmo 轴标签(vtkBillboardTextActor3D) FreeType
InteractionStyle InteractionStyle
FiltersSources FiltersSources
) )
@ -88,5 +89,8 @@ add_subdirectory(tools/gpr_poc)
# gpr3dv CLI vendored APIloadImpulseMultiChannel buildVolumeData runPipeline # gpr3dv CLI vendored APIloadImpulseMultiChannel buildVolumeData runPipeline
add_subdirectory(tools/gpr3dv_smoke) add_subdirectory(tools/gpr3dv_smoke)
# volbench CSV buildVolume /
add_subdirectory(tools/volbench)
enable_testing() enable_testing()
add_subdirectory(tests) add_subdirectory(tests)

View File

@ -1,6 +1,7 @@
#include "VtkSceneView.hpp" #include "VtkSceneView.hpp"
#include <algorithm> #include <algorithm>
#include <array>
#include <cmath> #include <cmath>
#include <memory> #include <memory>
#include <utility> #include <utility>
@ -95,9 +96,11 @@ VtkSceneView::~VtkSceneView() {
if (renderWindow_) { if (renderWindow_) {
if (auto* iren = renderWindow_->GetInteractor()) { if (auto* iren = renderWindow_->GetInteractor()) {
if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_); if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_);
if (gnomonHoverTag_ != 0) iren->RemoveObserver(gnomonHoverTag_);
} }
} }
gnomonClickTag_ = 0; gnomonClickTag_ = 0;
gnomonHoverTag_ = 0;
// 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。 // 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。
if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_); if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_);
gnomonCamTag_ = 0; gnomonCamTag_ = 0;
@ -510,25 +513,33 @@ void VtkSceneView::ensureGnomon() {
auto* iren = renderWindow_->GetInteractor(); auto* iren = renderWindow_->GetInteractor();
if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装 if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装
// 叠加渲染器图层1 固定右下角(x∈[0.85,1.0]、y∈[0.10,0.30]) —— 避开底部满宽沿线滑块条(仅雷达体 // 叠加渲染器图层1 固定右下角(见下 SetViewport) —— 避开底部满宽沿线滑块条(仅雷达体时显示、
// 时显示、约占底 46px)。透明背景只显 gizmo 图元FXAA 抗锯齿使边缘平滑;非交互不响应任何输入。 // 约占底 46px)。透明背景只显 gizmo 图元FXAA + MSAA 抗锯齿使边缘平滑;非交互不响应任何输入。
renderWindow_->SetNumberOfLayers(2); renderWindow_->SetNumberOfLayers(2);
gnomonRenderer_ = vtkSmartPointer<vtkRenderer>::New(); gnomonRenderer_ = vtkSmartPointer<vtkRenderer>::New();
gnomonRenderer_->SetLayer(1); gnomonRenderer_->SetLayer(1);
gnomonRenderer_->InteractiveOff(); gnomonRenderer_->InteractiveOff();
gnomonRenderer_->SetViewport(0.85, 0.10, 1.0, 0.30); // 右下角、上抬避开底部满宽「沿线位置」滑块条(约占底 46px)y 从 0.10 抬到 0.15;靠右留 ~1% 边距。
gnomonRenderer_->SetViewport(0.855, 0.15, 0.995, 0.35);
gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块 gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块
gnomonRenderer_->SetUseFXAA(true); // 抗锯齿:轴线/球边缘平滑 gnomonRenderer_->SetUseFXAA(true); // FXAA + 窗口 MSAA 双重:轴线/球/字形边缘平滑
renderWindow_->AddRenderer(gnomonRenderer_); renderWindow_->AddRenderer(gnomonRenderer_);
const double L = 1.0; // 球心到原点距离(= 轴线长度) // 抗锯齿(spec §7):整窗多重采样一次性开(仅当尚未开,不覆盖既有设置) → 平涂盘/白字边缘平滑。
if (renderWindow_->GetMultiSamples() == 0) renderWindow_->SetMultiSamples(8);
// 三根过原点的轴线X=红、Y=绿、Z=蓝(无光照纯色、加粗、不可拾取)。 const double L = 1.0; // 球心到原点距离(= 轴线长度)
struct AxisLine { double to[3]; double col[3]; }; // 业界柔和轴色(非纯 RGB)X 红 / Y 绿 / Z 蓝spec §2。正向球=本色,负向球=本色×0.42(更暗)。
const std::array<double, 3> kColX = {0.90, 0.30, 0.36};
const std::array<double, 3> kColY = {0.55, 0.78, 0.33};
const std::array<double, 3> kColZ = {0.28, 0.45, 0.90};
// 三根过原点的轴线仅连到【正向】球X=红 / Y=绿 / Z=蓝平涂纯色、细、不可拾取spec §5
struct AxisLine { double to[3]; std::array<double, 3> col; };
const AxisLine lines[3] = { const AxisLine lines[3] = {
{{L, 0, 0}, {0.90, 0.26, 0.26}}, // X 红 {{L, 0, 0}, kColX}, // X 红
{{0, L, 0}, {0.32, 0.78, 0.36}}, // Y 绿 {{0, L, 0}, kColY}, // Y 绿
{{0, 0, L}, {0.36, 0.56, 0.96}}, // Z 蓝 {{0, 0, L}, kColZ}, // Z 蓝
}; };
for (const auto& ln : lines) { for (const auto& ln : lines) {
vtkNew<vtkLineSource> src; vtkNew<vtkLineSource> src;
@ -539,62 +550,70 @@ void VtkSceneView::ensureGnomon() {
vtkNew<vtkActor> a; vtkNew<vtkActor> a;
a->SetMapper(mapper); a->SetMapper(mapper);
a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]); a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]);
a->GetProperty()->SetLineWidth(2.6f); a->GetProperty()->SetLineWidth(1.8f); // 细轴线
a->GetProperty()->SetLighting(false); a->GetProperty()->LightingOff(); // 平涂纯色,无高光
a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义) a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义)
gnomonRenderer_->AddViewProp(a); gnomonRenderer_->AddViewProp(a);
} }
// 6 个方向球:正向亮色填充 + XYZ 黑标签、稍大;负向暗色、稍小、无标签(业界导航 gizmo 风格) // 6 个方向球(平涂实心盘,非高光 3D 球):正向亮盘 + 白色 XYZ 字标、稍大;负向同色更暗、更小、无字标
// 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、Z=Bottom、+Y=Back、Y=Front、+X=Right、X=Left。 // 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、Z=Bottom、+Y=Back、Y=Front、+X=Right、X=Left。
struct DirSpec { struct DirSpec {
geopro::controller::ViewDir dir; geopro::controller::ViewDir dir;
double pos[3]; double pos[3];
double col[3]; std::array<double, 3> base; // 该轴柔和本色
bool positive; bool positive;
const char* label; // 正向;负向 nullptr const char* label; // 正向标;负向 nullptr
}; };
const DirSpec specs[6] = { const DirSpec specs[6] = {
{geopro::controller::ViewDir::Right, {L, 0, 0}, {0.92, 0.27, 0.27}, true, "X"}, // +X {geopro::controller::ViewDir::Right, {L, 0, 0}, kColX, true, "X"}, // +X
{geopro::controller::ViewDir::Left, {-L, 0, 0}, {0.46, 0.17, 0.17}, false, nullptr}, // X {geopro::controller::ViewDir::Left, {-L, 0, 0}, kColX, false, nullptr}, // X
{geopro::controller::ViewDir::Back, {0, L, 0}, {0.31, 0.78, 0.35}, true, "Y"}, // +Y {geopro::controller::ViewDir::Back, {0, L, 0}, kColY, true, "Y"}, // +Y
{geopro::controller::ViewDir::Front, {0, -L, 0}, {0.17, 0.41, 0.19}, false, nullptr}, // Y {geopro::controller::ViewDir::Front, {0, -L, 0}, kColY, false, nullptr}, // Y
{geopro::controller::ViewDir::Top, {0, 0, L}, {0.35, 0.57, 0.96}, true, "Z"}, // +Z {geopro::controller::ViewDir::Top, {0, 0, L}, kColZ, true, "Z"}, // +Z
{geopro::controller::ViewDir::Bottom, {0, 0, -L}, {0.19, 0.29, 0.52}, false, nullptr}, // Z {geopro::controller::ViewDir::Bottom, {0, 0, -L}, kColZ, false, nullptr}, // Z
}; };
gnomonDirs_.clear(); gnomonDirs_.clear();
gnomonBaseColor_.clear();
gnomonLabels_.clear();
for (const auto& s : specs) { for (const auto& s : specs) {
// 正向盘 r=0.32(稍大);负向盘 r=0.20(更小、更暗 → 呈"淡环/凹陷"观感,仍可拾取)。
const double radius = s.positive ? 0.32 : 0.20;
const std::array<double, 3> col =
s.positive ? s.base
: std::array<double, 3>{s.base[0] * 0.42, s.base[1] * 0.42, s.base[2] * 0.42};
vtkNew<vtkSphereSource> sphere; vtkNew<vtkSphereSource> sphere;
sphere->SetRadius(s.positive ? 0.30 : 0.22); // 正向大、负向小(hollow-looking) sphere->SetRadius(radius);
sphere->SetThetaResolution(32); sphere->SetThetaResolution(48); // 高分辨率 → 轮廓平滑(平涂下尤重要)
sphere->SetPhiResolution(32); sphere->SetPhiResolution(48);
sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]); sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]);
vtkNew<vtkPolyDataMapper> mapper; vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(sphere->GetOutputPort()); mapper->SetInputConnection(sphere->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::New(); auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper); actor->SetMapper(mapper);
auto* prop = actor->GetProperty(); auto* prop = actor->GetProperty();
prop->SetColor(s.col[0], s.col[1], s.col[2]); prop->SetColor(col[0], col[1], col[2]);
prop->SetAmbient(0.45); // 高环境光 → 颜色读数接近本色、球体仍有微弱立体感 prop->LightingOff(); // 关键(spec §1):平涂实心盘,无镜面/渐变 → 干净的 Blender 式 gizmo
prop->SetDiffuse(0.60); actor->SetOrigin(s.pos[0], s.pos[1], s.pos[2]); // 缩放原点=球心 → hover 放大就地不位移
prop->SetSpecular(0.20); actor->SetPickable(1); // 6 球均可拾取(负向点击仍 orbit 到对侧)
prop->SetSpecularPower(20.0);
actor->SetPickable(1); // 仅方向球可拾取
gnomonRenderer_->AddViewProp(actor); gnomonRenderer_->AddViewProp(actor);
gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向 gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向
gnomonBaseColor_[actor.Get()] = col; // 记本色 → hover 提亮后可复原
if (s.positive && s.label) { // 正向轴标签:始终朝相机的公告板文字,黑色粗体、居中于球心 if (s.positive && s.label) { // 正向轴字标:始终朝相机的公告板文字,白色粗体、居中于球心
auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::New(); auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::New();
lbl->SetInput(s.label); lbl->SetInput(s.label);
lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]); lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]);
auto* tp = lbl->GetTextProperty(); auto* tp = lbl->GetTextProperty();
tp->SetFontSize(15); tp->SetFontSize(20);
tp->SetBold(true); tp->SetBold(true);
tp->SetColor(0.06, 0.06, 0.06); tp->SetColor(1.0, 1.0, 1.0); // 白字
tp->SetJustificationToCentered(); tp->SetJustificationToCentered();
tp->SetVerticalJustificationToCentered(); tp->SetVerticalJustificationToCentered();
lbl->SetPickable(0); lbl->SetPickable(0);
gnomonRenderer_->AddViewProp(lbl); gnomonRenderer_->AddViewProp(lbl);
gnomonLabels_.push_back({lbl.Get(),
{s.pos[0], s.pos[1], s.pos[2]}}); // syncGnomonCamera 推到球前避遮挡
} }
} }
@ -608,6 +627,14 @@ void VtkSceneView::ensureGnomon() {
}); });
gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0); gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0);
// 鼠标移动高优先级观察者:仅角落内拾取做 hover 高亮,永不 abort → 不阻塞场景旋转/平移/切片交互。
gnomonHoverCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonHoverCmd_->SetClientData(this);
gnomonHoverCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->handleGnomonHover();
});
gnomonHoverTag_ = iren->AddObserver(vtkCommand::MouseMoveEvent, gnomonHoverCmd_, 1.0);
// 相机同步:观察主相机 ModifiedEvent每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。 // 相机同步:观察主相机 ModifiedEvent每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。
gnomonCamCmd_ = vtkSmartPointer<vtkCallbackCommand>::New(); gnomonCamCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonCamCmd_->SetClientData(this); gnomonCamCmd_->SetClientData(this);
@ -630,16 +657,36 @@ void VtkSceneView::syncGnomonCamera() {
if (!mainCam || !gcam) return; if (!mainCam || !gcam) return;
// 复制主相机投影方向 + view-upgizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。 // 复制主相机投影方向 + view-upgizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。
double dir[3]; double dir[3];
mainCam->GetDirectionOfProjection(dir); // 已归一化(FP) mainCam->GetDirectionOfProjection(dir); // 已归一化(FP),指向场景内(背离相机)
double up[3]; double up[3];
mainCam->GetViewUp(up); mainCam->GetViewUp(up);
const double dist = 10.0; const double dist = 10.0;
gcam->SetParallelProjection(1); // 正交投影gizmo 无透视畸变(业界标准) gcam->SetParallelProjection(1); // 正交投影gizmo 无透视畸变(业界标准)
gcam->SetParallelScale(1.8); // 取景半高:球到 ±(L+r)≈1.3 + 标签留边 // 按视口像素长宽比自适应取景半高balls 到 ±(L+r)≈1.32 + 白字留边 → halfExtent=1.5。
// parallelScale = 视口世界半高;水平可见半宽 = scale×aspect。取 scale=halfExtent/min(1,aspect)
// 保证长/宽两向都容得下所有球 → 任意窗口长宽比不裁切(视口归一化随窗拉伸也不失效)。
const double halfExtent = 1.5;
double aspect = 1.0;
const int* wsz = renderWindow_->GetSize();
const double* vp = gnomonRenderer_->GetViewport();
if (wsz && wsz[0] > 0 && wsz[1] > 0) {
const double vpH = (vp[3] - vp[1]) * wsz[1];
if (vpH > 0.0) aspect = (vp[2] - vp[0]) * wsz[0] / vpH;
}
gcam->SetParallelScale(halfExtent / std::min(1.0, aspect));
gcam->SetFocalPoint(0.0, 0.0, 0.0); gcam->SetFocalPoint(0.0, 0.0, 0.0);
gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist); gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist);
gcam->SetViewUp(up[0], up[1], up[2]); gcam->SetViewUp(up[0], up[1], up[2]);
gnomonRenderer_->ResetCameraClippingRange(); gnomonRenderer_->ResetCameraClippingRange();
// 正向白字标签推到球【前】(朝相机 = dir 方向、偏移略大于球半径)billboard 恒面相机,位于球前
// → 不被球面前半遮挡,读数清晰。相机每次转动都随之更新前向偏移。
const double front = 0.40; // > 正向球半径 0.32 → 字浮于球前表面之外
for (const auto& lp : gnomonLabels_) {
if (!lp.first) continue;
lp.first->SetPosition(lp.second[0] - dir[0] * front, lp.second[1] - dir[1] * front,
lp.second[2] - dir[2] * front);
}
} }
void VtkSceneView::handleGnomonClick() { void VtkSceneView::handleGnomonClick() {
@ -667,6 +714,58 @@ void VtkSceneView::handleGnomonClick() {
// 未命中球 → 不 abort左键继续走正常交互旋转/平移/缩放/切片/拾取),保证非干扰。 // 未命中球 → 不 abort左键继续走正常交互旋转/平移/缩放/切片/拾取),保证非干扰。
} }
void VtkSceneView::handleGnomonHover() {
// 非阻塞 hover 高亮:绝不 SetAbortFlag → 鼠标移动照常传给交互样式(旋转/平移/切片)。
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return;
// 提亮本色(向白混 0.42);复原用记录的本色。二者共用,避免重复。
auto applyColor = [](vtkProp* p, const std::array<double, 3>& c, bool highlight, double scale) {
auto* a = vtkActor::SafeDownCast(p);
if (!a) return;
if (highlight) {
a->GetProperty()->SetColor(c[0] * 0.58 + 0.42, c[1] * 0.58 + 0.42, c[2] * 0.58 + 0.42);
a->SetScale(scale); // 就地放大(origin=球心)
} else {
a->GetProperty()->SetColor(c[0], c[1], c[2]);
a->SetScale(1.0);
}
};
auto restore = [&]() {
if (!gnomonHovered_) return;
auto it = gnomonBaseColor_.find(gnomonHovered_);
if (it != gnomonBaseColor_.end()) applyColor(gnomonHovered_, it->second, false, 1.0);
gnomonHovered_ = nullptr;
};
const int ex = iren->GetEventPosition()[0];
const int ey = iren->GetEventPosition()[1];
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
const int* sz = renderWindow_->GetSize();
if (sz[0] <= 0 || sz[1] <= 0) { restore(); return; }
const double fx = static_cast<double>(ex) / sz[0];
const double fy = static_cast<double>(ey) / sz[1];
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) { // 光标离开角落 → 复原并跳过拾取(廉价)
if (gnomonHovered_) { restore(); renderWindow_->Render(); }
return;
}
// 角落内才做硬件拾取:命中方向球 → 高亮该球、复原旧的;未命中 → 复原。
vtkProp* hit = nullptr;
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
vtkProp* leaf = gnomonPicker_->GetViewProp();
if (gnomonBaseColor_.find(leaf) != gnomonBaseColor_.end()) hit = leaf;
}
if (hit == gnomonHovered_) return; // 无变化 → 不重绘
restore();
if (hit) {
auto it = gnomonBaseColor_.find(hit);
if (it != gnomonBaseColor_.end()) applyColor(hit, it->second, true, 1.18);
gnomonHovered_ = hit;
}
renderWindow_->Render(); // 高亮/复原变更 → 立即刷新
}
void VtkSceneView::rebuildAxes() { void VtkSceneView::rebuildAxes() {
// 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render + // 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render +
// 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元 // 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元

View File

@ -1,9 +1,11 @@
#pragma once #pragma once
#include <array>
#include <functional> #include <functional>
#include <map> #include <map>
#include <memory> #include <memory>
#include <set> #include <set>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
#include <vtkCubeAxesActor.h> #include <vtkCubeAxesActor.h>
@ -23,6 +25,7 @@ class vtkVolume;
class vtkPropPicker; class vtkPropPicker;
class vtkCallbackCommand; class vtkCallbackCommand;
class vtkCamera; class vtkCamera;
class vtkBillboardTextActor3D;
namespace geopro::app { namespace geopro::app {
@ -133,8 +136,12 @@ private:
// 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort // 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort
// (消费事件,阻止相机旋转/场景拾取);否则不 abort放行正常交互(旋转/平移/缩放/切片/拾取)。 // (消费事件,阻止相机旋转/场景拾取);否则不 abort放行正常交互(旋转/平移/缩放/切片/拾取)。
void handleGnomonClick(); void handleGnomonClick();
// 鼠标移动(非 abort、不阻塞场景交互)回调:仅当光标落在 gnomon 角落视口内才拾取,命中方向球 →
// 高亮(提亮本色 + 放大 ~1.18×),复原其余;离开角落或未命中 → 复原全部。picking 只在角落内进行(廉价)。
void handleGnomonHover();
// 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点) // 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点)
// 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。 // 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。
// 同时按视口像素长宽比自适应取景半高(球始终不裁切) + 把正向标签推到球前(朝相机)避免被球面遮挡。
void syncGnomonCamera(); void syncGnomonCamera();
public: public:
@ -214,6 +221,15 @@ private:
bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由叠加渲染器持有保活。 // 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由叠加渲染器持有保活。
std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_; std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_;
// ── hover 高亮spec §6─────────────────────────────────────────────────────
vtkSmartPointer<vtkCallbackCommand> gnomonHoverCmd_; // 鼠标移动观察者命令(不 abort非阻塞
unsigned long gnomonHoverTag_ = 0; // 移动观察者句柄(析构摘除)
vtkProp* gnomonHovered_ = nullptr; // 当前高亮的方向球裸指针renderer 保活)
std::map<vtkProp*, std::array<double, 3>> gnomonBaseColor_; // 各球本色hover 复原用)
// 正向标签(白字) + 其球心:每次 syncGnomonCamera 把标签推到球前(朝相机)→ 不被球面遮挡。
// raw ptr 非拥有,由叠加渲染器持有保活(与 gnomonDirs_ 同生命周期约定)。
std::vector<std::pair<vtkBillboardTextActor3D*, std::array<double, 3>>> gnomonLabels_;
}; };
} // namespace geopro::app } // namespace geopro::app