From b1a8e02f6d405fd21307604aa802b25678d6ee49 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 1 Jul 2026 10:45:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(vtk):=20=E8=A7=92=E8=90=BD=E5=8F=AF?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E6=96=B9=E5=90=91=E6=A0=87gnomon=E2=86=92?= =?UTF-8?q?=E7=BB=95=E5=BD=93=E5=89=8D=E8=BD=B4=E7=9B=92=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E8=BD=AC=E5=88=B0=E8=AF=A5=E8=BD=B4(=E4=BF=9D=E7=BC=A9?= =?UTF-8?q?=E6=94=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T3(spec 2026-07-01 §3.3/决策4·5):VtkSceneView 装配常驻右上角 gnomon (vtkOrientationMarkerWidget + vtkAxesActor 三向箭头/轴标签,相机随主相机同步)。 组装体内加 6 个方向球(±X/±Y/±Z, vtkPropAssembly)作可点击热区;左键高优先级 (1.0)观察者在角落视口内做 vtkPropPicker 硬件拾取,命中球→orbitToCurrentPivot (该方向)并 SetAbortFlag 消费(不触发相机旋转/场景拾取)。 orbitToCurrentPivot 封装决策5支点规则:有选中(useFittedAxes_)→子树盒 fittedBounds_ 中心,否则全场景 computeDataBounds 中心;无有效数据 no-op。 复用 T1 orbitToAxis/orbitPose(保当前缩放)。方向→ViewDir 对齐 CameraPreset: +Z=Top −Z=Bottom +Y=Back −Y=Front +X=Right −X=Left。 ensureGnomon 幂等(交互器就绪后于构造/render/renderIncremental 补装一次); 析构摘除观察者、禁用 widget。CMakeLists 增 RenderingAnnotation(vtkAxesActor) + FiltersSources(vtkSphereSource)。build.bat app 链接干净,test 473/473 通过。 --- src/app/CMakeLists.txt | 2 + src/app/VtkSceneView.cpp | 137 +++++++++++++++++++++++++++++++++++++++ src/app/VtkSceneView.hpp | 26 ++++++++ 3 files changed, 165 insertions(+) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 7efe7d7..58c6f63 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -5,8 +5,10 @@ find_package(VTK REQUIRED COMPONENTS GUISupportQt RenderingOpenGL2 RenderingVolumeOpenGL2 + RenderingAnnotation # vtkAxesActor(角落方向标 gnomon 三向箭头 + 轴标签) InteractionStyle InteractionWidgets + FiltersSources # vtkSphereSource(gnomon 6 向可点击方向球) FiltersGeometry FiltersModeling IOImage # vtkPNGWriter(切片导出图片) diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index b4b22d0..1d63aa0 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -9,17 +9,28 @@ #include #include +#include +#include +#include +#include #include +#include #include #include #include #include +#include +#include #include +#include +#include #include #include #include #include +#include #include +#include #include #include @@ -77,6 +88,19 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render // 近裁剪容差调小:场景含近处剖面 + 远处底图(几十km),默认容差会把近裁剪面随远面推出去、 // 切掉离相机近的剖面。调小后近面可贴近,剖面不被切(代价:远处深度精度略降,不可见层无所谓)。 scene_.renderer()->SetNearClippingPlaneTolerance(1e-5); + ensureGnomon(); // 交互器若已就绪即装配角落方向标(否则首帧 render 时补装) +} + +VtkSceneView::~VtkSceneView() { + // 摘除左键观察者(其 clientData=this,本对象析构后若留存会悬垂)+ 禁用 marker widget。 + // 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。 + if (renderWindow_) { + if (auto* iren = renderWindow_->GetInteractor()) { + if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_); + } + } + gnomonClickTag_ = 0; + if (gnomonWidget_) gnomonWidget_->SetEnabled(0); } void VtkSceneView::removeProps(std::vector>& props) { @@ -464,6 +488,117 @@ void VtkSceneView::orbitToAxis(geopro::controller::ViewDir dir, const double piv if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖 } +void VtkSceneView::orbitToCurrentPivot(geopro::controller::ViewDir dir) { + // 支点 = 当前坐标轴盒中心(决策 5):有选中(贴合轴)→选中子树盒 fittedBounds_;否则全场景数据盒。 + double b[6]; + if (useFittedAxes_) { + for (int i = 0; i < 6; ++i) b[i] = fittedBounds_[i]; + } else if (!computeDataBounds(b)) { + return; // 无有效数据包围盒 → 无支点可绕,静默不动 + } + const double pivot[3] = {0.5 * (b[0] + b[1]), 0.5 * (b[2] + b[3]), 0.5 * (b[4] + b[5])}; + orbitToAxis(dir, pivot); // 复用 T1:绕 pivot 转到 dir 轴、保留当前缩放 +} + +void VtkSceneView::ensureGnomon() { + // 幂等装配:交互器就绪后建一次。marker widget 把内部渲染器叠加到主渲染器另一图层,其相机随主相机 + // 同步 → 三向标随场景朝向转(widget 的核心价值)。6 个方向球组进同一 vtkPropAssembly 作可点击热区。 + if (gnomonReady_ || !renderWindow_) return; + auto* iren = renderWindow_->GetInteractor(); + if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装 + + // 经典 XYZ 三箭头 + 轴标签作视觉主体(不参与拾取,避免命中箭头无方向语义)。 + vtkNew axes; + axes->SetPickable(0); + axes->AxisLabelsOn(); + + vtkNew marker; + marker->AddPart(axes); + + // 6 个方向球:±X/±Y/±Z。位置对称于原点(widget 要求 marker 包围盒关于原点对称以正确同步相机)。 + // 正向球置于对应箭头尖端(与 axes 默认 TotalLength≈1 匹配)、稍亮;负向球在对侧、稍暗但仍可见可点。 + // 方向 → 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]; + }; + 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 + }; + gnomonDirs_.clear(); + for (const auto& s : specs) { + vtkNew sphere; + sphere->SetRadius(0.18); + sphere->SetThetaResolution(16); + sphere->SetPhiResolution(16); + 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 保活;此处仅记裸指针→方向 + } + + 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); + + gnomonPicker_ = vtkSmartPointer::New(); + + // 左键高优先级(1.0)观察者:先于交互样式(0.0),命中方向球 → orbit + abort 消费(阻止相机旋转/拾取)。 + gnomonClickCmd_ = vtkSmartPointer::New(); + gnomonClickCmd_->SetClientData(this); + gnomonClickCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) { + static_cast(client)->handleGnomonClick(); + }); + gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0); + + gnomonReady_ = true; +} + +void VtkSceneView::handleGnomonClick() { + if (!gnomonWidget_ || !gnomonPicker_ || !renderWindow_) return; + auto* iren = renderWindow_->GetInteractor(); + auto* gren = gnomonWidget_->GetRenderer(); + if (!iren || !gren) return; + const int ex = iren->GetEventPosition()[0]; + const int ey = iren->GetEventPosition()[1]; + // 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。 + const double* vp = gren->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(); + auto it = gnomonDirs_.find(leaf); + if (it != gnomonDirs_.end()) { + orbitToCurrentPivot(it->second); + if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 消费:不触发相机旋转/场景拾取 + } + } +} + void VtkSceneView::rebuildAxes() { // 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render + // 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。 @@ -514,6 +649,7 @@ void VtkSceneView::showSceneAxes() { } void VtkSceneView::render(bool is2D, bool resetCamera) { + ensureGnomon(); // 构造时交互器未就绪则于此补装(幂等) // 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。 double bgR, bgG, bgB; geopro::app::vtkBackground(bgR, bgG, bgB); @@ -539,6 +675,7 @@ void VtkSceneView::render(bool is2D, bool resetCamera) { } void VtkSceneView::renderIncremental() { + ensureGnomon(); // 幂等:交互器就绪后补装角落方向标 // 增量渲染:仅按新包围盒重建坐标轴并提交,不动相机(勾选/取消时视角不跳)。 rebuildAxes(); scene_.renderer()->ResetCameraClippingRange(); // 数据/底图变化后扩裁剪面,防被切 diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 8871a0f..792667d 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -20,6 +20,9 @@ class vtkRenderWindow; class vtkProp; class vtkActor; class vtkVolume; +class vtkOrientationMarkerWidget; +class vtkPropPicker; +class vtkCallbackCommand; namespace geopro::app { @@ -32,6 +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 void clear() override; void setVerticalExaggeration(double ve) override; @@ -75,6 +79,11 @@ public: void showFittedAxes(const double b[6]); // 取消选中 → 恢复全场景总览轴(现状默认行为,立即提交渲染)。 void showSceneAxes(); + // ── 可点击方向标 gnomon(spec §3.3;T3)───────────────────────────────────────── + // 绕【当前坐标轴盒中心】转到 dir 轴、保留当前缩放:支点 = 有选中(useFittedAxes_)→选中子树盒 + // fittedBounds_ 中心,否则全场景数据盒 computeDataBounds 中心。无有效数据 → no-op。 + // 封装决策 5 的支点规则,调用方只需给方向(角落 gnomon 点击即调此)。 + void orbitToCurrentPivot(geopro::controller::ViewDir dir); void render(bool is2D, bool resetCamera = true) override; void renderIncremental() override; @@ -117,6 +126,12 @@ private: void removeProps(std::vector>& props); // 从 renderer 移除并清空 // 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。 bool computeDataBounds(double out[6]) const; + // 角落可点击方向标 gnomon(T3):首次(交互器就绪)时装配 marker widget + 6 向可拾取球 + 左键观察者。 + // 幂等:装配后置 gnomonReady_,重复调直接返回。render/renderIncremental/构造均可安全调用。 + void ensureGnomon(); + // 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort + // (消费事件,阻止相机旋转/场景拾取);否则放行正常交互。 + void handleGnomonClick(); public: // 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。 @@ -180,6 +195,17 @@ private: // 哪些 dsProps_ 条目是 2D 足迹(addMapLine):供足迹 actor 归属识别(Task E2/F2 用)。 std::set mapLineDs_; + + // ── 可点击方向标 gnomon(T3)────────────────────────────────────────────────── + // marker widget(内部叠加渲染器,相机随主相机同步 → 三向标随场景转);SetInteractive(false) + // 禁自身拖动/缩放,仅作可点击方向选择器。gnomonPicker_ 在其内部渲染器上做硬件拾取。 + vtkSmartPointer gnomonWidget_; + vtkSmartPointer gnomonPicker_; + vtkSmartPointer gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费) + unsigned long gnomonClickTag_ = 0; // 观察者句柄(析构时摘除) + bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon) + // 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向)。actor 由 marker 组装体持有保活。 + std::map gnomonDirs_; }; } // namespace geopro::app