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
2 changed files with 137 additions and 61 deletions
Showing only changes of commit e1a10a1a73 - Show all commits

View File

@ -9,21 +9,19 @@
#include <QString>
#include <vtkActor.h>
#include <vtkAssemblyNode.h>
#include <vtkAssemblyPath.h>
#include <vtkAxesActor.h>
#include <vtkBillboardTextActor3D.h>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCommand.h>
#include <vtkProperty.h>
#include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h>
#include <vtkLineSource.h>
#include <vtkNew.h>
#include <vtkOrientationMarkerWidget.h>
#include <vtkPolyDataMapper.h>
#include <vtkProp.h>
#include <vtkPropAssembly.h>
#include <vtkPropPicker.h>
#include <vtkTextProperty.h>
#include <vtkPiecewiseFunction.h>
#include <vtkColorTransferFunction.h>
#include <vtkGPUVolumeRayCastMapper.h>
@ -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<vtkSmartPointer<vtkProp>>& 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<vtkAxesActor> 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<vtkRenderer>::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<vtkPropAssembly> 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<vtkLineSource> src;
src->SetPoint1(0.0, 0.0, 0.0);
src->SetPoint2(ln.to[0], ln.to[1], ln.to[2]);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(src->GetOutputPort());
vtkNew<vtkActor> 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<vtkSphereSource> 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<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 保活;此处仅记裸指针→方向
}
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<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);
if (s.positive && s.label) { // 正向轴标签:始终朝相机的公告板文字,黑色粗体、居中于球心
auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::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<vtkPropPicker>::New();
@ -567,36 +608,63 @@ void VtkSceneView::ensureGnomon() {
});
gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0);
// 相机同步:观察主相机 ModifiedEvent每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。
gnomonCamCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonCamCmd_->SetClientData(this);
gnomonCamCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(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-upgizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。
double dir[3];
mainCam->GetDirectionOfProjection(dir); // 已归一化(FP)
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<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();
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() {

View File

@ -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<geopro::core::GeoLocalFrame> 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<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
// 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。
bool computeDataBounds(double out[6]) const;
// 角落可点击方向标 gnomonT3首次(交互器就绪)时装配 marker widget + 6 向可拾取球 + 左键观察者。
// 角落可点击方向标 gnomonT3首次(交互器就绪)时装配【专用叠加渲染器】(图层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<std::string> mapLineDs_;
// ── 可点击方向标 gnomonT3──────────────────────────────────────────────────
// marker widget内部叠加渲染器相机随主相机同步 → 三向标随场景转SetInteractive(false)
// 禁自身拖动/缩放仅作可点击方向选择器。gnomonPicker_ 在其内部渲染器上做硬件拾取。
vtkSmartPointer<vtkOrientationMarkerWidget> gnomonWidget_;
// 专用叠加渲染器图层1、固定右下角视口、InteractiveOff、透明背景、无边框 —— 不是 widget
// 故无外框、不可拖动/缩放;相机由 syncGnomonCamera 镜像主相机朝向 → gizmo 随场景转。
// gnomonPicker_ 在此渲染器上做硬件拾取(仅方向球可拾取)。
vtkSmartPointer<vtkRenderer> gnomonRenderer_;
vtkSmartPointer<vtkPropPicker> gnomonPicker_;
vtkSmartPointer<vtkCallbackCommand> gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费)
unsigned long gnomonClickTag_ = 0; // 观察者句柄(析构时摘除)
unsigned long gnomonClickTag_ = 0; // 左键观察者句柄(析构时摘除)
vtkSmartPointer<vtkCallbackCommand> gnomonCamCmd_; // 主相机 ModifiedEvent 观察者命令
unsigned long gnomonCamTag_ = 0; // 相机观察者句柄(析构时摘除)
vtkCamera* gnomonObservedCam_ = nullptr; // 被观察的主相机(非拥有;析构摘观察者用)
bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由 marker 组装体持有保活。
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由叠加渲染器持有保活。
std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_;
};