diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 1d63aa0..4f24d84 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -9,21 +9,19 @@ #include #include -#include -#include -#include +#include #include #include #include #include #include #include +#include #include -#include #include #include -#include #include +#include #include #include #include @@ -92,7 +90,7 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render } VtkSceneView::~VtkSceneView() { - // 摘除左键观察者(其 clientData=this,本对象析构后若留存会悬垂)+ 禁用 marker widget。 + // 摘除左键/相机观察者(clientData=this,本对象析构后若留存会悬垂)+ 移除叠加渲染器。 // 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。 if (renderWindow_) { if (auto* iren = renderWindow_->GetInteractor()) { @@ -100,7 +98,11 @@ VtkSceneView::~VtkSceneView() { } } gnomonClickTag_ = 0; - if (gnomonWidget_) gnomonWidget_->SetEnabled(0); + // 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。 + if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_); + gnomonCamTag_ = 0; + gnomonObservedCam_ = nullptr; + if (renderWindow_ && gnomonRenderer_) renderWindow_->RemoveRenderer(gnomonRenderer_); } void VtkSceneView::removeProps(std::vector>& props) { @@ -501,61 +503,100 @@ void VtkSceneView::orbitToCurrentPivot(geopro::controller::ViewDir dir) { } void VtkSceneView::ensureGnomon() { - // 幂等装配:交互器就绪后建一次。marker widget 把内部渲染器叠加到主渲染器另一图层,其相机随主相机 - // 同步 → 三向标随场景朝向转(widget 的核心价值)。6 个方向球组进同一 vtkPropAssembly 作可点击热区。 + // 幂等装配:交互器就绪后建一次。用【专用叠加渲染器】(非 vtkOrientationMarkerWidget):图层1、固定 + // 右下角视口、InteractiveOff、透明背景 → 无 widget 外框、不可拖动/缩放;相机由 syncGnomonCamera + // 镜像主相机朝向 → gizmo 随场景旋转同步转。三轴线 + 6 方向球(仅球可拾取) + 正向 XYZ 标签。 if (gnomonReady_ || !renderWindow_) return; auto* iren = renderWindow_->GetInteractor(); if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装 - // 经典 XYZ 三箭头 + 轴标签作视觉主体(不参与拾取,避免命中箭头无方向语义)。 - vtkNew axes; - axes->SetPickable(0); - axes->AxisLabelsOn(); + // 叠加渲染器:图层1 固定右下角(x∈[0.85,1.0]、y∈[0.10,0.30]) —— 避开底部满宽沿线滑块条(仅雷达体 + // 时显示、约占底 46px)。透明背景只显 gizmo 图元;FXAA 抗锯齿使边缘平滑;非交互不响应任何输入。 + renderWindow_->SetNumberOfLayers(2); + gnomonRenderer_ = vtkSmartPointer::New(); + gnomonRenderer_->SetLayer(1); + gnomonRenderer_->InteractiveOff(); + gnomonRenderer_->SetViewport(0.85, 0.10, 1.0, 0.30); + gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块 + gnomonRenderer_->SetUseFXAA(true); // 抗锯齿:轴线/球边缘平滑 + renderWindow_->AddRenderer(gnomonRenderer_); - vtkNew marker; - marker->AddPart(axes); + const double L = 1.0; // 球心到原点距离(= 轴线长度) - // 6 个方向球:±X/±Y/±Z。位置对称于原点(widget 要求 marker 包围盒关于原点对称以正确同步相机)。 - // 正向球置于对应箭头尖端(与 axes 默认 TotalLength≈1 匹配)、稍亮;负向球在对侧、稍暗但仍可见可点。 + // 三根过原点的轴线:X=红、Y=绿、Z=蓝(无光照纯色、加粗、不可拾取)。 + struct AxisLine { double to[3]; double col[3]; }; + const AxisLine lines[3] = { + {{L, 0, 0}, {0.90, 0.26, 0.26}}, // X 红 + {{0, L, 0}, {0.32, 0.78, 0.36}}, // Y 绿 + {{0, 0, L}, {0.36, 0.56, 0.96}}, // Z 蓝 + }; + for (const auto& ln : lines) { + vtkNew src; + src->SetPoint1(0.0, 0.0, 0.0); + src->SetPoint2(ln.to[0], ln.to[1], ln.to[2]); + vtkNew mapper; + mapper->SetInputConnection(src->GetOutputPort()); + vtkNew a; + a->SetMapper(mapper); + a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]); + a->GetProperty()->SetLineWidth(2.6f); + a->GetProperty()->SetLighting(false); + a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义) + gnomonRenderer_->AddViewProp(a); + } + + // 6 个方向球:正向亮色填充 + XYZ 黑标签、稍大;负向暗色、稍小、无标签(业界导航 gizmo 风格)。 // 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、−Z=Bottom、+Y=Back、−Y=Front、+X=Right、−X=Left。 struct DirSpec { geopro::controller::ViewDir dir; double pos[3]; double col[3]; + bool positive; + const char* label; // 正向标签;负向 nullptr }; - const double L = 1.0; // 球心到原点距离(与轴长匹配) const DirSpec specs[6] = { - {geopro::controller::ViewDir::Right, {L, 0, 0}, {1.0, 0.30, 0.30}}, // +X - {geopro::controller::ViewDir::Left, {-L, 0, 0}, {0.55, 0.16, 0.16}}, // −X - {geopro::controller::ViewDir::Back, {0, L, 0}, {0.30, 1.0, 0.30}}, // +Y - {geopro::controller::ViewDir::Front, {0, -L, 0}, {0.16, 0.55, 0.16}}, // −Y - {geopro::controller::ViewDir::Top, {0, 0, L}, {0.40, 0.55, 1.0}}, // +Z - {geopro::controller::ViewDir::Bottom, {0, 0, -L}, {0.22, 0.30, 0.60}}, // −Z + {geopro::controller::ViewDir::Right, {L, 0, 0}, {0.92, 0.27, 0.27}, true, "X"}, // +X + {geopro::controller::ViewDir::Left, {-L, 0, 0}, {0.46, 0.17, 0.17}, false, nullptr}, // −X + {geopro::controller::ViewDir::Back, {0, L, 0}, {0.31, 0.78, 0.35}, true, "Y"}, // +Y + {geopro::controller::ViewDir::Front, {0, -L, 0}, {0.17, 0.41, 0.19}, false, nullptr}, // −Y + {geopro::controller::ViewDir::Top, {0, 0, L}, {0.35, 0.57, 0.96}, true, "Z"}, // +Z + {geopro::controller::ViewDir::Bottom, {0, 0, -L}, {0.19, 0.29, 0.52}, false, nullptr}, // −Z }; gnomonDirs_.clear(); for (const auto& s : specs) { vtkNew sphere; - sphere->SetRadius(0.18); - sphere->SetThetaResolution(16); - sphere->SetPhiResolution(16); + sphere->SetRadius(s.positive ? 0.30 : 0.22); // 正向大、负向小(hollow-looking) + sphere->SetThetaResolution(32); + sphere->SetPhiResolution(32); sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]); vtkNew mapper; mapper->SetInputConnection(sphere->GetOutputPort()); auto actor = vtkSmartPointer::New(); actor->SetMapper(mapper); - actor->GetProperty()->SetColor(s.col[0], s.col[1], s.col[2]); - actor->SetPickable(1); - marker->AddPart(actor); - gnomonDirs_[actor.Get()] = s.dir; // 组装体持 actor 保活;此处仅记裸指针→方向 - } + auto* prop = actor->GetProperty(); + prop->SetColor(s.col[0], s.col[1], s.col[2]); + prop->SetAmbient(0.45); // 高环境光 → 颜色读数接近本色、球体仍有微弱立体感 + prop->SetDiffuse(0.60); + prop->SetSpecular(0.20); + prop->SetSpecularPower(20.0); + actor->SetPickable(1); // 仅方向球可拾取 + gnomonRenderer_->AddViewProp(actor); + gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向 - gnomonWidget_ = vtkSmartPointer::New(); - gnomonWidget_->SetOrientationMarker(marker); - gnomonWidget_->SetDefaultRenderer(scene_.renderer()); // 父渲染器(相机同步源) - gnomonWidget_->SetInteractor(iren); - gnomonWidget_->SetViewport(0.82, 0.80, 1.0, 1.0); // 右上角:避开左上工具条 + 底部沿线条 - gnomonWidget_->SetInteractive(0); // 禁 widget 自身拖动/缩放,仅作方向选择器 - gnomonWidget_->SetEnabled(1); + if (s.positive && s.label) { // 正向轴标签:始终朝相机的公告板文字,黑色粗体、居中于球心 + auto lbl = vtkSmartPointer::New(); + lbl->SetInput(s.label); + lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]); + auto* tp = lbl->GetTextProperty(); + tp->SetFontSize(15); + tp->SetBold(true); + tp->SetColor(0.06, 0.06, 0.06); + tp->SetJustificationToCentered(); + tp->SetVerticalJustificationToCentered(); + lbl->SetPickable(0); + gnomonRenderer_->AddViewProp(lbl); + } + } gnomonPicker_ = vtkSmartPointer::New(); @@ -567,36 +608,63 @@ void VtkSceneView::ensureGnomon() { }); gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0); + // 相机同步:观察主相机 ModifiedEvent,每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。 + gnomonCamCmd_ = vtkSmartPointer::New(); + gnomonCamCmd_->SetClientData(this); + gnomonCamCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) { + static_cast(client)->syncGnomonCamera(); + }); + if (auto* mainCam = scene_.renderer() ? scene_.renderer()->GetActiveCamera() : nullptr) { + gnomonObservedCam_ = mainCam; + gnomonCamTag_ = mainCam->AddObserver(vtkCommand::ModifiedEvent, gnomonCamCmd_); + } + syncGnomonCamera(); // 初始一次:装配即与当前朝向对齐 + gnomonReady_ = true; } +void VtkSceneView::syncGnomonCamera() { + if (!gnomonRenderer_ || !scene_.renderer()) return; + auto* mainCam = scene_.renderer()->GetActiveCamera(); + auto* gcam = gnomonRenderer_->GetActiveCamera(); + if (!mainCam || !gcam) return; + // 复制主相机投影方向 + view-up:gizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。 + double dir[3]; + mainCam->GetDirectionOfProjection(dir); // 已归一化(F−P) + double up[3]; + mainCam->GetViewUp(up); + const double dist = 10.0; + gcam->SetParallelProjection(1); // 正交投影:gizmo 无透视畸变(业界标准) + gcam->SetParallelScale(1.8); // 取景半高:球到 ±(L+r)≈1.3 + 标签留边 + gcam->SetFocalPoint(0.0, 0.0, 0.0); + gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist); + gcam->SetViewUp(up[0], up[1], up[2]); + gnomonRenderer_->ResetCameraClippingRange(); +} + void VtkSceneView::handleGnomonClick() { - if (!gnomonWidget_ || !gnomonPicker_ || !renderWindow_) return; + if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return; auto* iren = renderWindow_->GetInteractor(); - auto* gren = gnomonWidget_->GetRenderer(); - if (!iren || !gren) return; + if (!iren) return; const int ex = iren->GetEventPosition()[0]; const int ey = iren->GetEventPosition()[1]; // 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。 - const double* vp = gren->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax](已含父视口换算) + const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax] const int* sz = renderWindow_->GetSize(); if (sz[0] <= 0 || sz[1] <= 0) return; const double fx = static_cast(ex) / sz[0]; const double fy = static_cast(ey) / sz[1]; - if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 放行 - // 角落内硬件拾取:命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。 - if (gnomonPicker_->PickProp(ex, ey, gren)) { - vtkProp* leaf = nullptr; - if (auto* path = gnomonPicker_->GetPath()) { - if (auto* node = path->GetLastNode()) leaf = node->GetViewProp(); // 组装体叶子=方向球 - } - if (!leaf) leaf = gnomonPicker_->GetViewProp(); + if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 不 abort,放行 + // 角落内硬件拾取(仅方向球可拾):命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。 + if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) { + vtkProp* leaf = gnomonPicker_->GetViewProp(); // 叠加渲染器内为裸 actor,直取即方向球 auto it = gnomonDirs_.find(leaf); if (it != gnomonDirs_.end()) { orbitToCurrentPivot(it->second); - if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 消费:不触发相机旋转/场景拾取 + if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 命中才消费:不触发相机旋转/场景拾取 } } + // 未命中球 → 不 abort:左键继续走正常交互(旋转/平移/缩放/切片/拾取),保证非干扰。 } void VtkSceneView::rebuildAxes() { diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 792667d..3c5da36 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -20,9 +20,9 @@ class vtkRenderWindow; class vtkProp; class vtkActor; class vtkVolume; -class vtkOrientationMarkerWidget; class vtkPropPicker; class vtkCallbackCommand; +class vtkCamera; namespace geopro::app { @@ -35,7 +35,7 @@ public: // 入参生命周期须覆盖本对象(由调用方保证)。zRefElev:地形 z 基准(测线地表高程)。 VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow, std::shared_ptr frame, double zRefElev); - ~VtkSceneView() override; // 摘除 gnomon 左键观察者(clientData=this),禁用 marker widget + ~VtkSceneView() override; // 摘除 gnomon 左键/相机观察者(clientData=this),移除叠加渲染器 void clear() override; void setVerticalExaggeration(double ve) override; @@ -126,12 +126,16 @@ private: void removeProps(std::vector>& props); // 从 renderer 移除并清空 // 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。 bool computeDataBounds(double out[6]) const; - // 角落可点击方向标 gnomon(T3):首次(交互器就绪)时装配 marker widget + 6 向可拾取球 + 左键观察者。 + // 角落可点击方向标 gnomon(T3):首次(交互器就绪)时装配【专用叠加渲染器】(图层1、固定右下角、 + // 非交互无边框) + 三轴线 + 6 向可拾取球 + 正向标签 + 左键/相机观察者。 // 幂等:装配后置 gnomonReady_,重复调直接返回。render/renderIncremental/构造均可安全调用。 void ensureGnomon(); // 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort - // (消费事件,阻止相机旋转/场景拾取);否则放行正常交互。 + // (消费事件,阻止相机旋转/场景拾取);否则不 abort,放行正常交互(旋转/平移/缩放/切片/拾取)。 void handleGnomonClick(); + // 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点), + // 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。 + void syncGnomonCamera(); public: // 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。 @@ -197,14 +201,18 @@ private: std::set mapLineDs_; // ── 可点击方向标 gnomon(T3)────────────────────────────────────────────────── - // marker widget(内部叠加渲染器,相机随主相机同步 → 三向标随场景转);SetInteractive(false) - // 禁自身拖动/缩放,仅作可点击方向选择器。gnomonPicker_ 在其内部渲染器上做硬件拾取。 - vtkSmartPointer gnomonWidget_; + // 专用叠加渲染器:图层1、固定右下角视口、InteractiveOff、透明背景、无边框 —— 不是 widget, + // 故无外框、不可拖动/缩放;相机由 syncGnomonCamera 镜像主相机朝向 → gizmo 随场景转。 + // gnomonPicker_ 在此渲染器上做硬件拾取(仅方向球可拾取)。 + vtkSmartPointer gnomonRenderer_; vtkSmartPointer gnomonPicker_; vtkSmartPointer gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费) - unsigned long gnomonClickTag_ = 0; // 观察者句柄(析构时摘除) + unsigned long gnomonClickTag_ = 0; // 左键观察者句柄(析构时摘除) + vtkSmartPointer gnomonCamCmd_; // 主相机 ModifiedEvent 观察者命令 + unsigned long gnomonCamTag_ = 0; // 相机观察者句柄(析构时摘除) + vtkCamera* gnomonObservedCam_ = nullptr; // 被观察的主相机(非拥有;析构摘观察者用) bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon) - // 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向)。actor 由 marker 组装体持有保活。 + // 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向)。actor 由叠加渲染器持有保活。 std::map gnomonDirs_; };