fix(vtk): 导航gnomon重做为固定右下角叠加渲染器(无边框可点转视角+业界样式)

This commit is contained in:
gaozheng 2026-07-01 11:52:31 +08:00
parent 0e449e082d
commit e1a10a1a73
2 changed files with 137 additions and 61 deletions

View File

@ -9,21 +9,19 @@
#include <QString> #include <QString>
#include <vtkActor.h> #include <vtkActor.h>
#include <vtkAssemblyNode.h> #include <vtkBillboardTextActor3D.h>
#include <vtkAssemblyPath.h>
#include <vtkAxesActor.h>
#include <vtkCallbackCommand.h> #include <vtkCallbackCommand.h>
#include <vtkCamera.h> #include <vtkCamera.h>
#include <vtkCommand.h> #include <vtkCommand.h>
#include <vtkProperty.h> #include <vtkProperty.h>
#include <vtkBoundingBox.h> #include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h> #include <vtkCubeAxesActor.h>
#include <vtkLineSource.h>
#include <vtkNew.h> #include <vtkNew.h>
#include <vtkOrientationMarkerWidget.h>
#include <vtkPolyDataMapper.h> #include <vtkPolyDataMapper.h>
#include <vtkProp.h> #include <vtkProp.h>
#include <vtkPropAssembly.h>
#include <vtkPropPicker.h> #include <vtkPropPicker.h>
#include <vtkTextProperty.h>
#include <vtkPiecewiseFunction.h> #include <vtkPiecewiseFunction.h>
#include <vtkColorTransferFunction.h> #include <vtkColorTransferFunction.h>
#include <vtkGPUVolumeRayCastMapper.h> #include <vtkGPUVolumeRayCastMapper.h>
@ -92,7 +90,7 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render
} }
VtkSceneView::~VtkSceneView() { VtkSceneView::~VtkSceneView() {
// 摘除左键观察者(clientData=this本对象析构后若留存会悬垂+ 禁用 marker widget // 摘除左键/相机观察者clientData=this本对象析构后若留存会悬垂+ 移除叠加渲染器
// 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。 // 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。
if (renderWindow_) { if (renderWindow_) {
if (auto* iren = renderWindow_->GetInteractor()) { if (auto* iren = renderWindow_->GetInteractor()) {
@ -100,7 +98,11 @@ VtkSceneView::~VtkSceneView() {
} }
} }
gnomonClickTag_ = 0; 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) { void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
@ -501,61 +503,100 @@ void VtkSceneView::orbitToCurrentPivot(geopro::controller::ViewDir dir) {
} }
void VtkSceneView::ensureGnomon() { void VtkSceneView::ensureGnomon() {
// 幂等装配交互器就绪后建一次。marker widget 把内部渲染器叠加到主渲染器另一图层,其相机随主相机 // 幂等装配:交互器就绪后建一次。用【专用叠加渲染器】(非 vtkOrientationMarkerWidget)图层1、固定
// 同步 → 三向标随场景朝向转widget 的核心价值。6 个方向球组进同一 vtkPropAssembly 作可点击热区。 // 右下角视口、InteractiveOff、透明背景 → 无 widget 外框、不可拖动/缩放;相机由 syncGnomonCamera
// 镜像主相机朝向 → gizmo 随场景旋转同步转。三轴线 + 6 方向球(仅球可拾取) + 正向 XYZ 标签。
if (gnomonReady_ || !renderWindow_) return; if (gnomonReady_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor(); auto* iren = renderWindow_->GetInteractor();
if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装 if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装
// 经典 XYZ 三箭头 + 轴标签作视觉主体(不参与拾取,避免命中箭头无方向语义)。 // 叠加渲染器图层1 固定右下角(x∈[0.85,1.0]、y∈[0.10,0.30]) —— 避开底部满宽沿线滑块条(仅雷达体
vtkNew<vtkAxesActor> axes; // 时显示、约占底 46px)。透明背景只显 gizmo 图元FXAA 抗锯齿使边缘平滑;非交互不响应任何输入。
axes->SetPickable(0); renderWindow_->SetNumberOfLayers(2);
axes->AxisLabelsOn(); 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; const double L = 1.0; // 球心到原点距离(= 轴线长度)
marker->AddPart(axes);
// 6 个方向球±X/±Y/±Z。位置对称于原点widget 要求 marker 包围盒关于原点对称以正确同步相机)。 // 三根过原点的轴线X=红、Y=绿、Z=蓝(无光照纯色、加粗、不可拾取)。
// 正向球置于对应箭头尖端(与 axes 默认 TotalLength≈1 匹配)、稍亮;负向球在对侧、稍暗但仍可见可点。 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。 // 方向 → 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]; double col[3];
bool positive;
const char* label; // 正向标签;负向 nullptr
}; };
const double L = 1.0; // 球心到原点距离(与轴长匹配)
const DirSpec specs[6] = { const DirSpec specs[6] = {
{geopro::controller::ViewDir::Right, {L, 0, 0}, {1.0, 0.30, 0.30}}, // +X {geopro::controller::ViewDir::Right, {L, 0, 0}, {0.92, 0.27, 0.27}, true, "X"}, // +X
{geopro::controller::ViewDir::Left, {-L, 0, 0}, {0.55, 0.16, 0.16}}, // 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.30, 1.0, 0.30}}, // +Y {geopro::controller::ViewDir::Back, {0, L, 0}, {0.31, 0.78, 0.35}, true, "Y"}, // +Y
{geopro::controller::ViewDir::Front, {0, -L, 0}, {0.16, 0.55, 0.16}}, // 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.40, 0.55, 1.0}}, // +Z {geopro::controller::ViewDir::Top, {0, 0, L}, {0.35, 0.57, 0.96}, true, "Z"}, // +Z
{geopro::controller::ViewDir::Bottom, {0, 0, -L}, {0.22, 0.30, 0.60}}, // Z {geopro::controller::ViewDir::Bottom, {0, 0, -L}, {0.19, 0.29, 0.52}, false, nullptr}, // Z
}; };
gnomonDirs_.clear(); gnomonDirs_.clear();
for (const auto& s : specs) { for (const auto& s : specs) {
vtkNew<vtkSphereSource> sphere; vtkNew<vtkSphereSource> sphere;
sphere->SetRadius(0.18); sphere->SetRadius(s.positive ? 0.30 : 0.22); // 正向大、负向小(hollow-looking)
sphere->SetThetaResolution(16); sphere->SetThetaResolution(32);
sphere->SetPhiResolution(16); sphere->SetPhiResolution(32);
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);
actor->GetProperty()->SetColor(s.col[0], s.col[1], s.col[2]); auto* prop = actor->GetProperty();
actor->SetPickable(1); prop->SetColor(s.col[0], s.col[1], s.col[2]);
marker->AddPart(actor); prop->SetAmbient(0.45); // 高环境光 → 颜色读数接近本色、球体仍有微弱立体感
gnomonDirs_[actor.Get()] = s.dir; // 组装体持 actor 保活;此处仅记裸指针→方向 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(); if (s.positive && s.label) { // 正向轴标签:始终朝相机的公告板文字,黑色粗体、居中于球心
gnomonWidget_->SetOrientationMarker(marker); auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::New();
gnomonWidget_->SetDefaultRenderer(scene_.renderer()); // 父渲染器(相机同步源) lbl->SetInput(s.label);
gnomonWidget_->SetInteractor(iren); lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]);
gnomonWidget_->SetViewport(0.82, 0.80, 1.0, 1.0); // 右上角:避开左上工具条 + 底部沿线条 auto* tp = lbl->GetTextProperty();
gnomonWidget_->SetInteractive(0); // 禁 widget 自身拖动/缩放,仅作方向选择器 tp->SetFontSize(15);
gnomonWidget_->SetEnabled(1); 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(); gnomonPicker_ = vtkSmartPointer<vtkPropPicker>::New();
@ -567,36 +608,63 @@ void VtkSceneView::ensureGnomon() {
}); });
gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0); 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; 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() { void VtkSceneView::handleGnomonClick() {
if (!gnomonWidget_ || !gnomonPicker_ || !renderWindow_) return; if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor(); auto* iren = renderWindow_->GetInteractor();
auto* gren = gnomonWidget_->GetRenderer(); if (!iren) return;
if (!iren || !gren) return;
const int ex = iren->GetEventPosition()[0]; const int ex = iren->GetEventPosition()[0];
const int ey = iren->GetEventPosition()[1]; const int ey = iren->GetEventPosition()[1];
// 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。 // 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。
const double* vp = gren->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax](已含父视口换算) const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
const int* sz = renderWindow_->GetSize(); const int* sz = renderWindow_->GetSize();
if (sz[0] <= 0 || sz[1] <= 0) return; if (sz[0] <= 0 || sz[1] <= 0) return;
const double fx = static_cast<double>(ex) / sz[0]; const double fx = static_cast<double>(ex) / sz[0];
const double fy = static_cast<double>(ey) / sz[1]; const double fy = static_cast<double>(ey) / sz[1];
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 放行 if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 不 abort放行
// 角落内硬件拾取:命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。 // 角落内硬件拾取(仅方向球可拾):命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。
if (gnomonPicker_->PickProp(ex, ey, gren)) { if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
vtkProp* leaf = nullptr; vtkProp* leaf = gnomonPicker_->GetViewProp(); // 叠加渲染器内为裸 actor直取即方向球
if (auto* path = gnomonPicker_->GetPath()) {
if (auto* node = path->GetLastNode()) leaf = node->GetViewProp(); // 组装体叶子=方向球
}
if (!leaf) leaf = gnomonPicker_->GetViewProp();
auto it = gnomonDirs_.find(leaf); auto it = gnomonDirs_.find(leaf);
if (it != gnomonDirs_.end()) { if (it != gnomonDirs_.end()) {
orbitToCurrentPivot(it->second); orbitToCurrentPivot(it->second);
if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 消费:不触发相机旋转/场景拾取 if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 命中才消费:不触发相机旋转/场景拾取
} }
} }
// 未命中球 → 不 abort左键继续走正常交互旋转/平移/缩放/切片/拾取),保证非干扰。
} }
void VtkSceneView::rebuildAxes() { void VtkSceneView::rebuildAxes() {

View File

@ -20,9 +20,9 @@ class vtkRenderWindow;
class vtkProp; class vtkProp;
class vtkActor; class vtkActor;
class vtkVolume; class vtkVolume;
class vtkOrientationMarkerWidget;
class vtkPropPicker; class vtkPropPicker;
class vtkCallbackCommand; class vtkCallbackCommand;
class vtkCamera;
namespace geopro::app { namespace geopro::app {
@ -35,7 +35,7 @@ public:
// 入参生命周期须覆盖本对象由调用方保证。zRefElev地形 z 基准(测线地表高程)。 // 入参生命周期须覆盖本对象由调用方保证。zRefElev地形 z 基准(测线地表高程)。
VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow, VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double zRefElev); 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 clear() override;
void setVerticalExaggeration(double ve) override; void setVerticalExaggeration(double ve) override;
@ -126,12 +126,16 @@ private:
void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空 void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
// 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。 // 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。
bool computeDataBounds(double out[6]) const; bool computeDataBounds(double out[6]) const;
// 角落可点击方向标 gnomonT3首次(交互器就绪)时装配 marker widget + 6 向可拾取球 + 左键观察者。 // 角落可点击方向标 gnomonT3首次(交互器就绪)时装配【专用叠加渲染器】(图层1、固定右下角、
// 非交互无边框) + 三轴线 + 6 向可拾取球 + 正向标签 + 左键/相机观察者。
// 幂等:装配后置 gnomonReady_重复调直接返回。render/renderIncremental/构造均可安全调用。 // 幂等:装配后置 gnomonReady_重复调直接返回。render/renderIncremental/构造均可安全调用。
void ensureGnomon(); void ensureGnomon();
// 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort // 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort
// (消费事件,阻止相机旋转/场景拾取);否则放行正常交互。 // (消费事件,阻止相机旋转/场景拾取);否则不 abort放行正常交互(旋转/平移/缩放/切片/拾取)
void handleGnomonClick(); void handleGnomonClick();
// 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点)
// 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。
void syncGnomonCamera();
public: public:
// 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。 // 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。
@ -197,14 +201,18 @@ private:
std::set<std::string> mapLineDs_; std::set<std::string> mapLineDs_;
// ── 可点击方向标 gnomonT3────────────────────────────────────────────────── // ── 可点击方向标 gnomonT3──────────────────────────────────────────────────
// marker widget内部叠加渲染器相机随主相机同步 → 三向标随场景转SetInteractive(false) // 专用叠加渲染器图层1、固定右下角视口、InteractiveOff、透明背景、无边框 —— 不是 widget
// 禁自身拖动/缩放仅作可点击方向选择器。gnomonPicker_ 在其内部渲染器上做硬件拾取。 // 故无外框、不可拖动/缩放;相机由 syncGnomonCamera 镜像主相机朝向 → gizmo 随场景转。
vtkSmartPointer<vtkOrientationMarkerWidget> gnomonWidget_; // gnomonPicker_ 在此渲染器上做硬件拾取(仅方向球可拾取)。
vtkSmartPointer<vtkRenderer> gnomonRenderer_;
vtkSmartPointer<vtkPropPicker> gnomonPicker_; vtkSmartPointer<vtkPropPicker> gnomonPicker_;
vtkSmartPointer<vtkCallbackCommand> gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费) 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 bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由 marker 组装体持有保活。 // 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由叠加渲染器持有保活。
std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_; std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_;
}; };