feat/vtk-merged-dataset-column #10

Merged
gaozheng merged 40 commits from feat/vtk-merged-dataset-column into main 2026-07-01 14:48:38 +08:00
3 changed files with 165 additions and 0 deletions
Showing only changes of commit b1a8e02f6d - Show all commits

View File

@ -5,8 +5,10 @@ find_package(VTK REQUIRED COMPONENTS
GUISupportQt
RenderingOpenGL2
RenderingVolumeOpenGL2
RenderingAnnotation # vtkAxesActor gnomon +
InteractionStyle
InteractionWidgets
FiltersSources # vtkSphereSourcegnomon 6
FiltersGeometry
FiltersModeling
IOImage # vtkPNGWriter

View File

@ -9,17 +9,28 @@
#include <QString>
#include <vtkActor.h>
#include <vtkAssemblyNode.h>
#include <vtkAssemblyPath.h>
#include <vtkAxesActor.h>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCommand.h>
#include <vtkProperty.h>
#include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h>
#include <vtkNew.h>
#include <vtkOrientationMarkerWidget.h>
#include <vtkPolyDataMapper.h>
#include <vtkProp.h>
#include <vtkPropAssembly.h>
#include <vtkPropPicker.h>
#include <vtkPiecewiseFunction.h>
#include <vtkColorTransferFunction.h>
#include <vtkGPUVolumeRayCastMapper.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSphereSource.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.h>
@ -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<vtkSmartPointer<vtkProp>>& 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<vtkAxesActor> axes;
axes->SetPickable(0);
axes->AxisLabelsOn();
vtkNew<vtkPropAssembly> 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<vtkSphereSource> sphere;
sphere->SetRadius(0.18);
sphere->SetThetaResolution(16);
sphere->SetPhiResolution(16);
sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(sphere->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::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<vtkOrientationMarkerWidget>::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<vtkPropPicker>::New();
// 左键高优先级(1.0)观察者:先于交互样式(0.0),命中方向球 → orbit + abort 消费(阻止相机旋转/拾取)。
gnomonClickCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonClickCmd_->SetClientData(this);
gnomonClickCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(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<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]) 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() {
// 先移除上一次的坐标轴 proprender 可能在一次 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(); // 数据/底图变化后扩裁剪面,防被切

View File

@ -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<geopro::core::GeoLocalFrame> 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();
// ── 可点击方向标 gnomonspec §3.3T3─────────────────────────────────────────
// 绕【当前坐标轴盒中心】转到 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<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
// 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。
bool computeDataBounds(double out[6]) const;
// 角落可点击方向标 gnomonT3首次(交互器就绪)时装配 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<std::string> mapLineDs_;
// ── 可点击方向标 gnomonT3──────────────────────────────────────────────────
// marker widget内部叠加渲染器相机随主相机同步 → 三向标随场景转SetInteractive(false)
// 禁自身拖动/缩放仅作可点击方向选择器。gnomonPicker_ 在其内部渲染器上做硬件拾取。
vtkSmartPointer<vtkOrientationMarkerWidget> gnomonWidget_;
vtkSmartPointer<vtkPropPicker> gnomonPicker_;
vtkSmartPointer<vtkCallbackCommand> gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费)
unsigned long gnomonClickTag_ = 0; // 观察者句柄(析构时摘除)
bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由 marker 组装体持有保活。
std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_;
};
} // namespace geopro::app