feat(vtk): P2 三维数据集栏 — 坐标轴/比例/快捷视图/Zoom

- CameraPreset 扩 6 向快捷视图(前后左右上下) + zoomBy(透视改视角/正交改parallelScale) + fitView
- AxesActor(新, vtkCubeAxesActor): 显示模式 标准(外缘)/三维立体(静态边+网格线)/不显示;
  刻度 无/米/英尺(×3.28084)/经纬度(GeoLocalFrame反算); 字号12(字体待1.0确认)
- GeoLocalFrame 补 toLatLon 反算(等距圆柱)
- I3dSceneView 扩 setAxes/applyCameraView/zoom/fitView; VtkSceneController 加对应槽
  (坐标轴随场景重建; 快捷视图/zoom 仅改相机不重建)
- main.cpp 三维视图工具条: 坐标轴/刻度下拉(枚举绑itemData)+比例滑块(1-10)+6向钮+Zoom钮, 仅3D显示
- 测试 +24(toLatLon往返/相机6向/坐标轴单位换算/控制器编排), ctest 196/196

评审修复:
- HIGH rebuildAxes 异步路径坐标轴 prop 累积 → 持 currentAxes_ 重建前先移除(幂等)
- MEDIUM combo index 魔数 → itemData/currentData 取枚举值(防项序调整静默错位)

注: 坐标轴标准/立体语义 + 字体 + O点 为合理近似, 待 Geopro 1.0 实地确认精修
This commit is contained in:
gaozheng 2026-06-15 21:54:48 +08:00
parent 2c204a134a
commit 3dea339ddc
18 changed files with 890 additions and 8 deletions

View File

@ -3,6 +3,7 @@
#include <utility> #include <utility>
#include <vtkActor.h> #include <vtkActor.h>
#include <vtkCubeAxesActor.h>
#include <vtkRenderWindow.h> #include <vtkRenderWindow.h>
#include <vtkRenderer.h> #include <vtkRenderer.h>
#include <vtkVolume.h> #include <vtkVolume.h>
@ -10,6 +11,7 @@
#include "CameraPreset.hpp" #include "CameraPreset.hpp"
#include "Scene.hpp" #include "Scene.hpp"
#include "Theme.hpp" #include "Theme.hpp"
#include "actors/AxesActor.hpp"
#include "actors/CurtainActor.hpp" #include "actors/CurtainActor.hpp"
#include "actors/MapLineActor.hpp" #include "actors/MapLineActor.hpp"
#include "actors/TerrainActor.hpp" #include "actors/TerrainActor.hpp"
@ -18,6 +20,38 @@
namespace geopro::app { namespace geopro::app {
namespace {
// 控制器层枚举 → render 层枚举(保持控制器不依赖 render
geopro::render::AxesMode toRenderMode(geopro::controller::AxesMode m) {
switch (m) {
case geopro::controller::AxesMode::Standard: return geopro::render::AxesMode::Standard;
case geopro::controller::AxesMode::Stereo: return geopro::render::AxesMode::Stereo;
case geopro::controller::AxesMode::None: return geopro::render::AxesMode::None;
}
return geopro::render::AxesMode::Standard;
}
geopro::render::AxesUnit toRenderUnit(geopro::controller::AxesUnit u) {
switch (u) {
case geopro::controller::AxesUnit::None: return geopro::render::AxesUnit::None;
case geopro::controller::AxesUnit::Meter: return geopro::render::AxesUnit::Meter;
case geopro::controller::AxesUnit::Feet: return geopro::render::AxesUnit::Feet;
case geopro::controller::AxesUnit::LatLon: return geopro::render::AxesUnit::LatLon;
}
return geopro::render::AxesUnit::Meter;
}
geopro::render::ViewDir toRenderViewDir(geopro::controller::ViewDir d) {
switch (d) {
case geopro::controller::ViewDir::Front: return geopro::render::ViewDir::Front;
case geopro::controller::ViewDir::Back: return geopro::render::ViewDir::Back;
case geopro::controller::ViewDir::Left: return geopro::render::ViewDir::Left;
case geopro::controller::ViewDir::Right: return geopro::render::ViewDir::Right;
case geopro::controller::ViewDir::Top: return geopro::render::ViewDir::Top;
case geopro::controller::ViewDir::Bottom: return geopro::render::ViewDir::Bottom;
}
return geopro::render::ViewDir::Front;
}
} // namespace
VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow, VtkSceneView::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)
: scene_(scene), : scene_(scene),
@ -25,7 +59,10 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render
frame_(std::move(frame)), frame_(std::move(frame)),
zRefElev_(zRefElev) {} zRefElev_(zRefElev) {}
void VtkSceneView::clear() { scene_.clear(); } void VtkSceneView::clear() {
scene_.clear(); // RemoveAllViewProps连同坐标轴一并移除
currentAxes_ = nullptr; // 旧坐标轴已随 clear 移除,置空避免悬留引用
}
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; } void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
@ -56,11 +93,58 @@ void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
if (terrain) scene_.addActor(terrain); if (terrain) scene_.addActor(terrain);
} }
void VtkSceneView::setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
int fontSize) {
axesMode_ = mode;
axesUnit_ = unit;
axesFontSize_ = fontSize;
}
void VtkSceneView::applyCameraView(geopro::controller::ViewDir dir) {
geopro::render::applyView(scene_.renderer(), toRenderViewDir(dir));
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::zoom(double factor) {
geopro::render::zoomBy(scene_.renderer(), factor);
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::fitView() {
geopro::render::fitView(scene_.renderer());
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::rebuildAxes() {
// 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render +
// 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元
if (currentAxes_) {
scene_.renderer()->RemoveViewProp(currentAxes_);
currentAxes_ = nullptr;
}
// 坐标轴随数据包围盒重建:按已加入的数据图元算 bounds再造 vtkCubeAxesActor 入场。
// None 模式或无内容 → buildAxes 返回 nullptr场景无坐标轴。
double bounds[6];
scene_.renderer()->ComputeVisiblePropBounds(bounds);
geopro::render::AxesOptions opts;
opts.mode = toRenderMode(axesMode_);
opts.unit = toRenderUnit(axesUnit_);
opts.fontSize = axesFontSize_;
opts.frame = frame_.get();
auto axes = geopro::render::buildAxes(bounds, opts, scene_.renderer());
if (axes) {
scene_.addViewProp(axes);
currentAxes_ = axes;
}
}
void VtkSceneView::render(bool is2D) { void VtkSceneView::render(bool is2D) {
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。 // 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
double bgR, bgG, bgB; double bgR, bgG, bgB;
geopro::app::vtkBackground(bgR, bgG, bgB); geopro::app::vtkBackground(bgR, bgG, bgB);
scene_.renderer()->SetBackground(bgR, bgG, bgB); scene_.renderer()->SetBackground(bgR, bgG, bgB);
// 坐标轴仅三维视图显示2D 俯视测线不需要立体坐标轴)。
if (!is2D) rebuildAxes();
if (is2D) if (is2D)
geopro::render::applyTop2D(scene_.renderer()); geopro::render::applyTop2D(scene_.renderer());
else else

View File

@ -1,6 +1,9 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <vtkCubeAxesActor.h>
#include <vtkSmartPointer.h>
#include "I3dSceneView.hpp" #include "I3dSceneView.hpp"
namespace geopro::core { class GeoLocalFrame; } namespace geopro::core { class GeoLocalFrame; }
@ -26,14 +29,30 @@ public:
void addCurtain(const geopro::core::Grid& grid, const geopro::core::ColorScale& cs) override; void addCurtain(const geopro::core::Grid& grid, const geopro::core::ColorScale& cs) override;
void addVolume(const geopro::data::VolumeGrid& vol, const geopro::core::ColorScale& cs) override; void addVolume(const geopro::data::VolumeGrid& vol, const geopro::core::ColorScale& cs) override;
void addTerrain(const geopro::data::TerrainPaths& paths) override; void addTerrain(const geopro::data::TerrainPaths& paths) override;
void setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
int fontSize) override;
void applyCameraView(geopro::controller::ViewDir dir) override;
void zoom(double factor) override;
void fitView() override;
void render(bool is2D) override; void render(bool is2D) override;
private: private:
// 按当前坐标轴设置 + 场景包围盒重建坐标轴 proprender 末尾调)。
void rebuildAxes();
geopro::render::Scene& scene_; geopro::render::Scene& scene_;
vtkRenderWindow* renderWindow_; vtkRenderWindow* renderWindow_;
std::shared_ptr<geopro::core::GeoLocalFrame> frame_; std::shared_ptr<geopro::core::GeoLocalFrame> frame_;
double zRefElev_; double zRefElev_;
double verticalExaggeration_ = 2.0; double verticalExaggeration_ = 2.0;
// 坐标轴设置P2默认标准 + 米。
geopro::controller::AxesMode axesMode_ = geopro::controller::AxesMode::Standard;
geopro::controller::AxesUnit axesUnit_ = geopro::controller::AxesUnit::Meter;
int axesFontSize_ = 12;
// 当前坐标轴 proprender 可能多次调用 rebuildAxesrebuild 末尾 + 异步回灌),
// 持引用以便重建前移除旧 prop避免叠加评审 HIGH
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
}; };
} // namespace geopro::app } // namespace geopro::app

View File

@ -34,8 +34,11 @@
#include <QFile> #include <QFile>
#include <QButtonGroup> #include <QButtonGroup>
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox>
#include <QFrame> #include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPushButton>
#include <QSlider>
#include <QGraphicsOpacityEffect> #include <QGraphicsOpacityEffect>
#include <QDate> #include <QDate>
#include <QLabel> #include <QLabel>
@ -181,6 +184,34 @@ private:
QWidget* host_; QWidget* host_;
}; };
// 把浮层锚定在 host 右上角P2 三维数据集栏工具条):随 host 尺寸变化重定位,
// 紧贴右边距 14px置于 header 下方 12px。仅在浮层可见时移动隐藏时不打扰
class RightTopAnchor : public QObject {
public:
RightTopAnchor(QWidget* overlay, QWidget* host, QWidget* header)
: QObject(host), overlay_(overlay), host_(host), header_(header)
{
host_->installEventFilter(this);
}
protected:
bool eventFilter(QObject* obj, QEvent* e) override
{
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show) &&
overlay_->isVisible()) {
overlay_->adjustSize();
overlay_->move(host_->width() - overlay_->width() - 14, header_->height() + 12);
overlay_->raise();
}
return QObject::eventFilter(obj, e);
}
private:
QWidget* overlay_;
QWidget* host_;
QWidget* header_;
};
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。 // 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
std::string readPem(const std::string& path) std::string readPem(const std::string& path)
{ {
@ -365,6 +396,80 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
layerLayout->addWidget(chkTerrain); layerLayout->addWidget(chkTerrain);
layerPanel->setVisible(false); // 默认二维,不显示图层浮层 layerPanel->setVisible(false); // 默认二维,不显示图层浮层
// ──「三维数据集栏」工具条浮层P2spec §7.2):浮于 QVTK 右上,仅三维视图显示。
// 坐标轴下拉(标准/立体/不显示) + 刻度下拉(无/米/英尺/经纬度) + 纵向比例滑块 + 快捷视图 6 钮 + Zoom(In/Out/Fit)。
auto* axisBar = new QFrame(centerWidget);
axisBar->setFrameShape(QFrame::StyledPanel);
geopro::app::applyTokenizedStyleSheet(
axisBar,
QStringLiteral("QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};}"
"QLabel{color:{{canvas/text}};border:none;background:transparent;}"
"QComboBox{color:{{canvas/text}};}"
"QPushButton{color:{{canvas/text}};padding:2px 6px;}"));
auto* axisLayout = new QHBoxLayout(axisBar);
axisLayout->setContentsMargins(geopro::app::space::kMd, geopro::app::space::kSm,
geopro::app::space::kMd, geopro::app::space::kSm);
axisLayout->setSpacing(geopro::app::space::kSm);
// 坐标轴显示方式下拉(枚举值绑到 itemData槽用 currentData 取值,不依赖项顺序)。
auto* axesModeCombo = new QComboBox();
axesModeCombo->addItem(QStringLiteral("坐标轴:标准"),
static_cast<int>(geopro::controller::AxesMode::Standard));
axesModeCombo->addItem(QStringLiteral("坐标轴:三维立体"),
static_cast<int>(geopro::controller::AxesMode::Stereo));
axesModeCombo->addItem(QStringLiteral("坐标轴:不显示"),
static_cast<int>(geopro::controller::AxesMode::None));
// 刻度单位下拉。
auto* axesUnitCombo = new QComboBox();
axesUnitCombo->addItem(QStringLiteral("刻度:无"),
static_cast<int>(geopro::controller::AxesUnit::None));
axesUnitCombo->addItem(QStringLiteral("刻度:米"),
static_cast<int>(geopro::controller::AxesUnit::Meter));
axesUnitCombo->addItem(QStringLiteral("刻度:英尺"),
static_cast<int>(geopro::controller::AxesUnit::Feet));
axesUnitCombo->addItem(QStringLiteral("刻度:经纬度"),
static_cast<int>(geopro::controller::AxesUnit::LatLon));
axesUnitCombo->setCurrentIndex(1); // 默认米(与控制器默认一致)
// 纵向比例滑块(范围 110默认 2spec §4 C6
auto* veLabel = new QLabel(QStringLiteral("比例"));
auto* veSlider = new QSlider(Qt::Horizontal);
veSlider->setMinimum(1);
veSlider->setMaximum(10);
veSlider->setValue(static_cast<int>(kVerticalExaggeration));
veSlider->setFixedWidth(80);
auto* veValue = new QLabel(QStringLiteral("%1x").arg(static_cast<int>(kVerticalExaggeration)));
// 快捷视图 6 钮。
auto* btnTop = new QPushButton(QStringLiteral(""));
auto* btnBottom = new QPushButton(QStringLiteral(""));
auto* btnFront = new QPushButton(QStringLiteral(""));
auto* btnBack = new QPushButton(QStringLiteral(""));
auto* btnLeft = new QPushButton(QStringLiteral(""));
auto* btnRight = new QPushButton(QStringLiteral(""));
// Zoom 3 钮。
auto* btnZoomIn = new QPushButton(QStringLiteral("放大"));
auto* btnZoomOut = new QPushButton(QStringLiteral("缩小"));
auto* btnFit = new QPushButton(QStringLiteral("适配"));
axisLayout->addWidget(axesModeCombo);
axisLayout->addWidget(axesUnitCombo);
axisLayout->addWidget(veLabel);
axisLayout->addWidget(veSlider);
axisLayout->addWidget(veValue);
axisLayout->addWidget(btnFront);
axisLayout->addWidget(btnBack);
axisLayout->addWidget(btnLeft);
axisLayout->addWidget(btnRight);
axisLayout->addWidget(btnTop);
axisLayout->addWidget(btnBottom);
axisLayout->addWidget(btnZoomIn);
axisLayout->addWidget(btnZoomOut);
axisLayout->addWidget(btnFit);
axisBar->setVisible(false); // 默认二维,不显示
// P2 工具条右上锚定:随 centerWidget 尺寸变化重定位(紧贴右边距 14px工具条下方
// 锚定器 parent=centerWidget随其销毁不需保留指针。
new RightTopAnchor(axisBar, centerWidget, viewHeader);
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。── // ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
// 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中; // 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中;
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。 // 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
@ -565,15 +670,22 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
} }
}); });
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。 // 「视图详情」浮层 + 「三维数据集栏」工具条显隐:仅三维显示。
auto showLayerPanel = [layerPanel, viewHeader](bool show3D) { // 视图详情浮层置左上P2 工具条置右上(工具条下方),二者均随相机/数据变化保持位置。
auto showLayerPanel = [layerPanel, axisBar, viewHeader, centerWidget](bool show3D) {
if (show3D) { if (show3D) {
layerPanel->move(14, viewHeader->height() + 12); layerPanel->move(14, viewHeader->height() + 12);
layerPanel->adjustSize(); layerPanel->adjustSize();
layerPanel->setVisible(true); layerPanel->setVisible(true);
layerPanel->raise(); layerPanel->raise();
axisBar->adjustSize();
// 右上对齐:紧贴右边距 14px。
axisBar->move(centerWidget->width() - axisBar->width() - 14, viewHeader->height() + 12);
axisBar->setVisible(true);
axisBar->raise();
} else { } else {
layerPanel->setVisible(false); layerPanel->setVisible(false);
axisBar->setVisible(false);
} }
}; };
@ -599,6 +711,44 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget, QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget,
[sceneCtrl](bool on) { sceneCtrl->setLayer(SceneLayer::Terrain, on); }); [sceneCtrl](bool on) { sceneCtrl->setLayer(SceneLayer::Terrain, on); });
// ──「三维数据集栏」工具条 → 控制器槽P2──
using geopro::controller::AxesMode;
using geopro::controller::AxesUnit;
using geopro::controller::ViewDir;
QObject::connect(axesModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), sceneCtrl,
[sceneCtrl, axesModeCombo](int) {
sceneCtrl->setAxesMode(
static_cast<AxesMode>(axesModeCombo->currentData().toInt()));
});
QObject::connect(axesUnitCombo, qOverload<int>(&QComboBox::currentIndexChanged), sceneCtrl,
[sceneCtrl, axesUnitCombo](int) {
sceneCtrl->setAxesUnit(
static_cast<AxesUnit>(axesUnitCombo->currentData().toInt()));
});
QObject::connect(veSlider, &QSlider::valueChanged, sceneCtrl,
[sceneCtrl, veValue](int v) {
veValue->setText(QStringLiteral("%1x").arg(v));
sceneCtrl->setVerticalExaggeration(static_cast<double>(v));
});
QObject::connect(btnFront, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->applyView(ViewDir::Front); });
QObject::connect(btnBack, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->applyView(ViewDir::Back); });
QObject::connect(btnLeft, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->applyView(ViewDir::Left); });
QObject::connect(btnRight, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->applyView(ViewDir::Right); });
QObject::connect(btnTop, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->applyView(ViewDir::Top); });
QObject::connect(btnBottom, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->applyView(ViewDir::Bottom); });
QObject::connect(btnZoomIn, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->zoomIn(); });
QObject::connect(btnZoomOut, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->zoomOut(); });
QObject::connect(btnFit, &QPushButton::clicked, sceneCtrl,
[sceneCtrl]() { sceneCtrl->fit(); });
// ── 左上对象树勾选 → 渲染勾选数据集(本期样本驱动:任意勾选 → 样本 ds "grid1",空 → 清场)── // ── 左上对象树勾选 → 渲染勾选数据集(本期样本驱动:任意勾选 → 样本 ds "grid1",空 → 清场)──
// 真实接 Api 时改为把勾选 TM 映射到其 ds 维度过滤后的真实 dsId 列表spec §6.1/§8 // 真实接 Api 时改为把勾选 TM 映射到其 ds 维度过滤后的真实 dsId 列表spec §6.1/§8
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, sceneCtrl, QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, sceneCtrl,

View File

@ -5,6 +5,13 @@
namespace geopro::controller { namespace geopro::controller {
// 坐标轴显示方式spec §4 C3I3标准 / 三维立体 / 不显示。
enum class AxesMode { Standard, Stereo, None };
// 坐标轴刻度单位spec §4 D5I5无 / 米 / 英尺 / 经纬度。
enum class AxesUnit { None, Meter, Feet, LatLon };
// 快捷视图方向spec §4 C7前/后/左/右/上/下。
enum class ViewDir { Front, Back, Left, Right, Top, Bottom };
// 三维场景视图抽象(编排层与 VTK 渲染解耦的缝): // 三维场景视图抽象(编排层与 VTK 渲染解耦的缝):
// VtkSceneController 只发出"清场 / 加某类图元 / 提交渲染"指令,不认 vtkActor/vtkVolume // VtkSceneController 只发出"清场 / 加某类图元 / 提交渲染"指令,不认 vtkActor/vtkVolume
// 真实实现VtkSceneView调 render actor + Scene测试用 fake 记录调用断言编排。 // 真实实现VtkSceneView调 render actor + Scene测试用 fake 记录调用断言编排。
@ -27,6 +34,17 @@ public:
// 3DDEM 地形 + 影像纹理。 // 3DDEM 地形 + 影像纹理。
virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0; virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0;
// 坐标轴设置P2显示方式 + 刻度单位 + 字号。视图据当前场景包围盒重建坐标轴 prop。
// None 模式 = 移除坐标轴rebuild 时由控制器在 clear 后重新下发当前坐标轴设置。
virtual void setAxes(AxesMode mode, AxesUnit unit, int fontSize) = 0;
// 快捷视图P2应用 6 向相机预设并提交渲染。
virtual void applyCameraView(ViewDir dir) = 0;
// 缩放P2factor>1 放大、<1 缩小,提交渲染。
virtual void zoom(double factor) = 0;
// 适配全览P2ResetCamera 并提交渲染。
virtual void fitView() = 0;
// 应用相机预设2D 俯视 / 3D 自由)并提交渲染。 // 应用相机预设2D 俯视 / 3D 自由)并提交渲染。
virtual void render(bool is2D) = 0; virtual void render(bool is2D) = 0;
}; };

View File

@ -42,6 +42,22 @@ void VtkSceneController::setVerticalExaggeration(double ve) {
void VtkSceneController::rebuild() { rebuildInternal(); } void VtkSceneController::rebuild() { rebuildInternal(); }
void VtkSceneController::setAxesMode(AxesMode mode) {
axesMode_ = mode;
rebuildInternal(); // 坐标轴随场景重建clear 会移除旧坐标轴 prop
}
void VtkSceneController::setAxesUnit(AxesUnit unit) {
axesUnit_ = unit;
rebuildInternal();
}
// 快捷视图 / 缩放:仅改相机,不重建场景(无须取数/重装图元)。
void VtkSceneController::applyView(ViewDir dir) { view_.applyCameraView(dir); }
void VtkSceneController::zoomIn() { view_.zoom(1.2); }
void VtkSceneController::zoomOut() { view_.zoom(1.0 / 1.2); }
void VtkSceneController::fit() { view_.fitView(); }
const geopro::core::Grid& VtkSceneController::grid(const std::string& dsId) { const geopro::core::Grid& VtkSceneController::grid(const std::string& dsId) {
auto it = gridCache_.find(dsId); auto it = gridCache_.find(dsId);
if (it == gridCache_.end()) it = gridCache_.emplace(dsId, dsRepo_.loadGrid(dsId)).first; if (it == gridCache_.end()) it = gridCache_.emplace(dsId, dsRepo_.loadGrid(dsId)).first;
@ -61,6 +77,8 @@ void VtkSceneController::rebuildInternal() {
view_.clear(); view_.clear();
view_.setVerticalExaggeration(verticalExaggeration_); view_.setVerticalExaggeration(verticalExaggeration_);
// 坐标轴设置在 clear 后下发render 末尾据当前场景包围盒重建坐标轴 prop。
view_.setAxes(axesMode_, axesUnit_, kAxesFontSize);
inRebuild_ = true; inRebuild_ = true;
// 坏 dsIdloadGrid/loadColorScale 抛异常)= best-effort 跳过emit loadFailed 但不中断, // 坏 dsIdloadGrid/loadColorScale 抛异常)= best-effort 跳过emit loadFailed 但不中断,

View File

@ -6,6 +6,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include "I3dSceneView.hpp"
#include "model/ColorScale.hpp" #include "model/ColorScale.hpp"
#include "model/Field.hpp" #include "model/Field.hpp"
#include "repo/I3dSceneRepository.hpp" #include "repo/I3dSceneRepository.hpp"
@ -16,8 +17,6 @@ class IDatasetRepository;
namespace geopro::controller { namespace geopro::controller {
class I3dSceneView;
// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。 // 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。
enum class ViewMode { Map2D, View3D }; enum class ViewMode { Map2D, View3D };
@ -42,6 +41,14 @@ public slots:
void setVerticalExaggeration(double ve); void setVerticalExaggeration(double ve);
void rebuild(); // 主题切换等外部触发的重渲染 void rebuild(); // 主题切换等外部触发的重渲染
// ── P2 三维数据集栏 ──
void setAxesMode(AxesMode mode);
void setAxesUnit(AxesUnit unit);
void applyView(ViewDir dir); // 6 向快捷视图
void zoomIn(); // Zoom In (×1.2)
void zoomOut(); // Zoom Out (×1/1.2)
void fit(); // Fit (ResetCamera)
signals: signals:
void loadFailed(const QString& message); void loadFailed(const QString& message);
@ -59,6 +66,11 @@ private:
bool showTerrain_ = false; bool showTerrain_ = false;
double verticalExaggeration_ = 2.0; double verticalExaggeration_ = 2.0;
// 坐标轴设置P2默认标准 + 米;字号固定 12字体设置待 1.0 确认)。
AxesMode axesMode_ = AxesMode::Standard;
AxesUnit axesUnit_ = AxesUnit::Meter;
static constexpr int kAxesFontSize = 12;
// 缓存(按 dsId避免重复读盘/插值。 // 缓存(按 dsId避免重复读盘/插值。
std::map<std::string, geopro::core::Grid> gridCache_; std::map<std::string, geopro::core::Grid> gridCache_;
std::map<std::string, geopro::core::ColorScale> colorScaleCache_; std::map<std::string, geopro::core::ColorScale> colorScaleCache_;

View File

@ -21,4 +21,8 @@ LocalXY GeoLocalFrame::toLocal(double lat, double lon) const {
return LocalXY{(lon - lon0_) * mPerDegLon_, (lat - lat0_) * mPerDegLat_}; return LocalXY{(lon - lon0_) * mPerDegLon_, (lat - lat0_) * mPerDegLat_};
} }
LatLon GeoLocalFrame::toLatLon(double x, double y) const {
return LatLon{lat0_ + y / mPerDegLat_, lon0_ + x / mPerDegLon_};
}
} // namespace geopro::core } // namespace geopro::core

View File

@ -2,6 +2,7 @@
namespace geopro::core { namespace geopro::core {
struct LocalXY { double x, y; }; struct LocalXY { double x, y; };
struct LatLon { double lat, lon; };
// 等距圆柱(equirectangular)近似:把经纬度投到以(lat0,lon0)为原点的局部米平面。 // 等距圆柱(equirectangular)近似:把经纬度投到以(lat0,lon0)为原点的局部米平面。
// 小范围测区足够x=East、y=North(米)。 // 小范围测区足够x=East、y=North(米)。
@ -9,6 +10,9 @@ class GeoLocalFrame {
public: public:
GeoLocalFrame(double lat0, double lon0); GeoLocalFrame(double lat0, double lon0);
LocalXY toLocal(double lat, double lon) const; // -> (x East m, y North m) LocalXY toLocal(double lat, double lon) const; // -> (x East m, y North m)
// toLocal 的反算:局部米 (x East, y North) -> 经纬度。
// lon = lon0 + x/mPerDegLonlat = lat0 + y/mPerDegLat坐标轴经纬度刻度用
LatLon toLatLon(double x, double y) const;
private: private:
double lat0_, lon0_, mPerDegLon_, mPerDegLat_; double lat0_, lon0_, mPerDegLon_, mPerDegLat_;
}; };

View File

@ -1,7 +1,7 @@
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets IOImage) find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 RenderingAnnotation InteractionStyle InteractionWidgets IOImage)
find_package(GDAL CONFIG REQUIRED) find_package(GDAL CONFIG REQUIRED)
add_library(geopro_render STATIC add_library(geopro_render STATIC
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp) Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp)
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL) target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
target_compile_features(geopro_render PUBLIC cxx_std_17) target_compile_features(geopro_render PUBLIC cxx_std_17)

View File

@ -37,4 +37,54 @@ void applyFree3D(vtkRenderer* r)
r->ResetCamera(); r->ResetCamera();
} }
void applyView(vtkRenderer* r, ViewDir dir)
{
if (!r) return;
auto* c = r->GetActiveCamera();
// 6 向均为正交快捷视图。焦点先置原点ResetCamera 再按场景重定位相机距离;
// 方向由 (position-focalPoint) 与 viewUp 决定(世界系 x=East,y=North,z=-depth
c->SetFocalPoint(0, 0, 0);
switch (dir) {
case ViewDir::Top: // 俯视:相机在 +Z 向下看,北(+Y)朝上
c->SetPosition(0, 0, 1);
c->SetViewUp(0, 1, 0);
break;
case ViewDir::Bottom: // 仰视:相机在 -Z 向上看
c->SetPosition(0, 0, -1);
c->SetViewUp(0, 1, 0);
break;
case ViewDir::Front: // 北望:相机在 -Y 看向 +Y上(+Z)朝上
c->SetPosition(0, -1, 0);
c->SetViewUp(0, 0, 1);
break;
case ViewDir::Back: // 南望:相机在 +Y 看向 -Y
c->SetPosition(0, 1, 0);
c->SetViewUp(0, 0, 1);
break;
case ViewDir::Left: // 东望:相机在 -X 看向 +X
c->SetPosition(-1, 0, 0);
c->SetViewUp(0, 0, 1);
break;
case ViewDir::Right: // 西望:相机在 +X 看向 -X
c->SetPosition(1, 0, 0);
c->SetViewUp(0, 0, 1);
break;
}
c->OrthogonalizeViewUp();
r->ResetCamera();
}
void zoomBy(vtkRenderer* r, double factor)
{
if (!r || factor <= 0.0) return;
// vtkCamera::Zoom 同时覆盖透视(改视角)与正交(改 parallelScale)factor>1 放大。
r->GetActiveCamera()->Zoom(factor);
}
void fitView(vtkRenderer* r)
{
if (!r) return;
r->ResetCamera();
}
} // namespace geopro::render } // namespace geopro::render

View File

@ -8,4 +8,20 @@ void applyTop2D(vtkRenderer* r);
// 自由三维:透视投影,斜视方位看到剖面立体。 // 自由三维:透视投影,斜视方位看到剖面立体。
void applyFree3D(vtkRenderer* r); void applyFree3D(vtkRenderer* r);
// 快捷视图方向(世界系 x=East,y=North,z=-depth
// Top 俯视 (相机在 +Z 向下看)
// Bottom 仰视 (相机在 -Z 向上看)
// Front 从 -Y 看向 +Y (北望)Back 反向
// Left 从 -X 看向 +X (东望)Right 反向
enum class ViewDir { Front, Back, Left, Right, Top, Bottom };
// 应用 6 向正交快捷视图:设 position/focalPoint/viewUp 后 ResetCamera。
void applyView(vtkRenderer* r, ViewDir dir);
// 相机缩放factor>1 拉近(放大)factor<1 推远(缩小)。透视下改距离、正交下改 parallelScale。
void zoomBy(vtkRenderer* r, double factor);
// 适配场景ResetCamera全览
void fitView(vtkRenderer* r);
} // namespace geopro::render } // namespace geopro::render

View File

@ -0,0 +1,98 @@
#include "actors/AxesActor.hpp"
#include <vtkCubeAxesActor.h>
#include <vtkRenderer.h>
#include <vtkTextProperty.h>
namespace geopro::render {
namespace {
constexpr double kFeetPerMeter = 3.28084;
// 包围盒退化判定:任一轴 min>max或六值全 0无内容
bool boundsDegenerate(const double b[6]) {
if (b[0] > b[1] || b[2] > b[3] || b[4] > b[5]) return true;
for (int i = 0; i < 6; ++i)
if (b[i] != 0.0) return false;
return true; // 全 0
}
// 设三轴标题字号/标签字号(待 1.0 字体确认,先统一 fontSize
void applyFont(vtkCubeAxesActor* ax, int fontSize) {
for (int i = 0; i < 3; ++i) {
if (auto* t = ax->GetTitleTextProperty(i)) t->SetFontSize(fontSize);
if (auto* l = ax->GetLabelTextProperty(i)) l->SetFontSize(fontSize);
}
}
} // namespace
double unitScaleFactor(AxesUnit unit) {
switch (unit) {
case AxesUnit::Meter: return 1.0;
case AxesUnit::Feet: return kFeetPerMeter;
case AxesUnit::None:
case AxesUnit::LatLon: return 1.0; // None 隐藏标签LatLon 非线性,单独处理
}
return 1.0;
}
vtkSmartPointer<vtkCubeAxesActor> buildAxes(const double bounds[6], const AxesOptions& opts,
vtkRenderer* renderer) {
if (opts.mode == AxesMode::None) return nullptr;
if (!bounds || boundsDegenerate(bounds)) return nullptr;
auto ax = vtkSmartPointer<vtkCubeAxesActor>::New();
double b[6];
for (int i = 0; i < 6; ++i) b[i] = bounds[i];
ax->SetBounds(b);
if (renderer) ax->SetCamera(renderer->GetActiveCamera());
// 显示模式:标准=外侧最近边;三维立体=静态边(四周更完整闭合,近似立方)+ 网格线。
if (opts.mode == AxesMode::Stereo) {
ax->SetFlyModeToStaticEdges();
ax->DrawXGridlinesOn();
ax->DrawYGridlinesOn();
ax->DrawZGridlinesOn();
} else { // Standard
ax->SetFlyModeToOuterEdges();
}
// 刻度标签None 隐藏;其余按单位换算「显示值范围」(几何 bounds 不变,仅标签数值变)。
if (opts.unit == AxesUnit::None) {
ax->SetXAxisLabelVisibility(false);
ax->SetYAxisLabelVisibility(false);
ax->SetZAxisLabelVisibility(false);
} else if (opts.unit == AxesUnit::LatLon && opts.frame) {
// 经纬度X→经度、Y→纬度用 frame 反算 bounds 端点Z 退化为米深度。
// bounds 布局 {xmin,xmax,ymin,ymax,zmin,zmax}(b[0],b[2])=西南角、(b[1],b[3])=东北角。
// 等距圆柱投影单调 → 角点经纬度即为各轴显示范围端点。
auto ll0 = opts.frame->toLatLon(b[0], b[2]);
auto ll1 = opts.frame->toLatLon(b[1], b[3]);
ax->SetXAxisRange(ll0.lon, ll1.lon);
ax->SetYAxisRange(ll0.lat, ll1.lat);
ax->SetZAxisRange(b[4], b[5]);
ax->SetXTitle("Lon");
ax->SetYTitle("Lat");
ax->SetZTitle("Depth(m)");
ax->SetXLabelFormat("%.5f");
ax->SetYLabelFormat("%.5f");
} else {
// 米 / 英尺:显示范围 = 几何范围 × 系数。
const double s = unitScaleFactor(opts.unit);
ax->SetXAxisRange(b[0] * s, b[1] * s);
ax->SetYAxisRange(b[2] * s, b[3] * s);
ax->SetZAxisRange(b[4] * s, b[5] * s);
const char* u = (opts.unit == AxesUnit::Feet) ? "ft" : "m";
ax->SetXTitle("X");
ax->SetYTitle("Y");
ax->SetZTitle("Z");
ax->SetXUnits(u);
ax->SetYUnits(u);
ax->SetZUnits(u);
}
applyFont(ax, opts.fontSize);
return ax;
}
} // namespace geopro::render

View File

@ -0,0 +1,44 @@
#pragma once
#include <vtkSmartPointer.h>
#include "geo/GeoLocalFrame.hpp"
class vtkCubeAxesActor;
class vtkRenderer;
namespace geopro::render {
// 坐标轴显示方式spec §4 C3I3
// Standard 标准 = vtkCubeAxesActor 包围盒 + 刻度(外侧最近轴显示刻度)。
// Stereo 三维立体 = vtkCubeAxesActor 闭合立方(四周/网格更完整)。语义待 1.0 确认,先合理近似。
// None 不显示 = 不构建(返回 nullptr
enum class AxesMode { Standard, Stereo, None };
// 刻度单位spec §4 D5I5
// None 无刻度 = 隐藏刻度标签。
// Meter 米 = 原值(世界系本就是米)。
// Feet 英尺 = ×3.28084。
// LatLon 经纬度 = 经 GeoLocalFrame 反算 X→经度、Y→纬度Z 退化为米深度)。
enum class AxesUnit { None, Meter, Feet, LatLon };
// 坐标轴构建参数。
struct AxesOptions {
AxesMode mode = AxesMode::Standard;
AxesUnit unit = AxesUnit::Meter;
int fontSize = 12; // 标题/标签字号
// 经纬度刻度需 frame 反算;为空则 LatLon 退化为米。
const geopro::core::GeoLocalFrame* frame = nullptr;
};
// 由数据包围盒 bounds[6]={xmin,xmax,ymin,ymax,zmin,zmax} + 选项构建坐标轴 prop。
// O 点 = 数据包围盒角(待 1.0 确认spec §13 倾向"数据包围盒角")。
// bounds 退化min>max 或全 0或 mode==None → 返回 nullptr。
// cameravtkCubeAxesActor 需绑定相机(决定外侧刻度轴);可空(测试场景)。
vtkSmartPointer<vtkCubeAxesActor> buildAxes(const double bounds[6], const AxesOptions& opts,
vtkRenderer* renderer);
// 单位换算系数米→目标单位。LatLon 不是线性系数X/Y 分别反算),此处仅供米/英尺;
// 暴露为可测纯函数。
double unitScaleFactor(AxesUnit unit);
} // namespace geopro::render

View File

@ -77,7 +77,7 @@ endif()
# render ColorLutBuildercore ColorScale -> vtkLookupTable # render ColorLutBuildercore ColorScale -> vtkLookupTable
# vtkLookupTableVTK::CommonCoregeopro_render PUBLIC VTK # vtkLookupTableVTK::CommonCoregeopro_render PUBLIC VTK
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore) find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore RenderingAnnotation FiltersSources)
# SceneaddActor/addViewProp + clear vtkVolume addViewProp # SceneaddActor/addViewProp + clear vtkVolume addViewProp
target_sources(geopro_tests PRIVATE render/test_scene.cpp) target_sources(geopro_tests PRIVATE render/test_scene.cpp)
target_sources(geopro_tests PRIVATE render/test_color_lut.cpp) target_sources(geopro_tests PRIVATE render/test_color_lut.cpp)
@ -96,6 +96,10 @@ target_sources(geopro_tests PRIVATE render/test_anomaly.cpp)
target_sources(geopro_tests PRIVATE render/test_electrode.cpp) target_sources(geopro_tests PRIVATE render/test_electrode.cpp)
# TerrainbuildTerrain(GDAL dem/image + 重投影 warp 面+纹理) /缺文件安全( PROJ_DATA) # TerrainbuildTerrain(GDAL dem/image + 重投影 warp 面+纹理) /缺文件安全( PROJ_DATA)
target_sources(geopro_tests PRIVATE render/test_terrain.cpp) target_sources(geopro_tests PRIVATE render/test_terrain.cpp)
# CameraPreset(P2)6 position/focalPoint/viewUp + zoomBy /parallelScale
target_sources(geopro_tests PRIVATE render/test_camera_preset.cpp)
# AxesActor(P2)buildAxes(bounds+unit/mode→vtkCubeAxesActor) 单位换算(英尺/经纬度)/
target_sources(geopro_tests PRIVATE render/test_axes.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})

View File

@ -27,6 +27,17 @@ struct FakeView : I3dSceneView {
bool lastIs2D = false; bool lastIs2D = false;
double ve = -1.0; double ve = -1.0;
// P2 记录。
int setAxesCalls = 0;
AxesMode lastAxesMode = AxesMode::None;
AxesUnit lastAxesUnit = AxesUnit::None;
int lastAxesFont = -1;
int cameraViewCalls = 0;
ViewDir lastViewDir = ViewDir::Front;
int zoomCalls = 0;
double lastZoomFactor = 0.0;
int fitCalls = 0;
// clear 模型化"移除所有图元"图元计数归零反映当前场景状态clears 累加。 // clear 模型化"移除所有图元"图元计数归零反映当前场景状态clears 累加。
void clear() override { void clear() override {
++clears; ++clears;
@ -37,6 +48,13 @@ struct FakeView : I3dSceneView {
void addCurtain(const core::Grid&, const core::ColorScale&) override { ++curtains; } void addCurtain(const core::Grid&, const core::ColorScale&) override { ++curtains; }
void addVolume(const data::VolumeGrid&, const core::ColorScale&) override { ++volumes; } void addVolume(const data::VolumeGrid&, const core::ColorScale&) override { ++volumes; }
void addTerrain(const data::TerrainPaths&) override { ++terrains; } void addTerrain(const data::TerrainPaths&) override { ++terrains; }
void setAxes(AxesMode mode, AxesUnit unit, int fontSize) override {
++setAxesCalls;
lastAxesMode = mode; lastAxesUnit = unit; lastAxesFont = fontSize;
}
void applyCameraView(ViewDir dir) override { ++cameraViewCalls; lastViewDir = dir; }
void zoom(double factor) override { ++zoomCalls; lastZoomFactor = factor; }
void fitView() override { ++fitCalls; }
void render(bool is2D) override { ++renders; lastIs2D = is2D; } void render(bool is2D) override { ++renders; lastIs2D = is2D; }
int props() const { return surveyLines + curtains + volumes + terrains; } int props() const { return surveyLines + curtains + volumes + terrains; }
@ -165,3 +183,67 @@ TEST(VtkSceneController, MultipleDatasetsAddMultipleCurtains) {
c.setCheckedDatasets({"ds1", "ds2", "ds3"}); c.setCheckedDatasets({"ds1", "ds2", "ds3"});
EXPECT_EQ(view.curtains, 3); EXPECT_EQ(view.curtains, 3);
} }
// ── P2坐标轴 / 快捷视图 / Zoom 编排 ──
// 每次重建都把当前坐标轴设置下发给视图clear 后须重设)。
TEST(VtkSceneController, RebuildForwardsAxesSettings) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D); // 触发一次重建
EXPECT_GE(view.setAxesCalls, 1);
// 默认 = 标准 + 米 + 字号 12。
EXPECT_EQ(view.lastAxesMode, AxesMode::Standard);
EXPECT_EQ(view.lastAxesUnit, AxesUnit::Meter);
EXPECT_EQ(view.lastAxesFont, 12);
}
// setAxesMode 改模式并重建下发。
TEST(VtkSceneController, SetAxesModeForwardedOnRebuild) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setAxesMode(AxesMode::None);
EXPECT_EQ(view.lastAxesMode, AxesMode::None);
const int rebuilds = view.setAxesCalls;
c.setAxesMode(AxesMode::Stereo);
EXPECT_EQ(view.lastAxesMode, AxesMode::Stereo);
EXPECT_GT(view.setAxesCalls, rebuilds); // 又触发一次重建
}
// setAxesUnit 改单位并重建下发。
TEST(VtkSceneController, SetAxesUnitForwarded) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setAxesUnit(AxesUnit::Feet);
EXPECT_EQ(view.lastAxesUnit, AxesUnit::Feet);
c.setAxesUnit(AxesUnit::LatLon);
EXPECT_EQ(view.lastAxesUnit, AxesUnit::LatLon);
}
// applyView 转发方向,不重建场景(不增 clear
TEST(VtkSceneController, ApplyViewForwardsDirectionWithoutRebuild) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
const int clearsBefore = view.clears;
c.applyView(ViewDir::Top);
EXPECT_EQ(view.cameraViewCalls, 1);
EXPECT_EQ(view.lastViewDir, ViewDir::Top);
EXPECT_EQ(view.clears, clearsBefore); // 不重建
c.applyView(ViewDir::Left);
EXPECT_EQ(view.lastViewDir, ViewDir::Left);
}
// zoomIn/zoomOut 用 1.2 / (1/1.2)fit 调 fitView。
TEST(VtkSceneController, ZoomAndFitForwarded) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.zoomIn();
EXPECT_EQ(view.zoomCalls, 1);
EXPECT_DOUBLE_EQ(view.lastZoomFactor, 1.2);
c.zoomOut();
EXPECT_DOUBLE_EQ(view.lastZoomFactor, 1.0 / 1.2);
c.fit();
EXPECT_EQ(view.fitCalls, 1);
}

View File

@ -37,3 +37,25 @@ TEST(GeoFrame, NorthwardLatitudeGivesPositiveY) {
EXPECT_NEAR(p.y, expected, expected * 0.05); EXPECT_NEAR(p.y, expected, expected * 0.05);
EXPECT_NEAR(p.x, 0.0, 1e-9); EXPECT_NEAR(p.x, 0.0, 1e-9);
} }
// toLatLon 是 toLocal 的反算toLocal∘toLatLon 与 toLatLon∘toLocal 都恒等。
TEST(GeoFrame, ToLatLonRoundTrips) {
GeoLocalFrame f(22.5, 114.16);
// 经纬度 → 局部 → 经纬度 恒等。
auto p = f.toLocal(22.53, 114.19);
auto ll = f.toLatLon(p.x, p.y);
EXPECT_NEAR(ll.lat, 22.53, 1e-9);
EXPECT_NEAR(ll.lon, 114.19, 1e-9);
// 局部 → 经纬度 → 局部 恒等。
auto q = f.toLocal(ll.lat, ll.lon);
EXPECT_NEAR(q.x, p.x, 1e-6);
EXPECT_NEAR(q.y, p.y, 1e-6);
}
// 原点局部 (0,0) 反算回 (lat0,lon0)。
TEST(GeoFrame, ToLatLonOriginMapsToLat0Lon0) {
GeoLocalFrame f(22.5, 114.16);
auto ll = f.toLatLon(0.0, 0.0);
EXPECT_NEAR(ll.lat, 22.5, 1e-12);
EXPECT_NEAR(ll.lon, 114.16, 1e-12);
}

105
tests/render/test_axes.cpp Normal file
View File

@ -0,0 +1,105 @@
#include <gtest/gtest.h>
#include <vtkCubeAxesActor.h>
#include "actors/AxesActor.hpp"
#include "geo/GeoLocalFrame.hpp"
using namespace geopro::render;
namespace {
constexpr double kFeetPerMeter = 3.28084;
}
// unitScaleFactor米=1英尺=3.28084。
TEST(AxesActor, UnitScaleFactor) {
EXPECT_DOUBLE_EQ(unitScaleFactor(AxesUnit::Meter), 1.0);
EXPECT_DOUBLE_EQ(unitScaleFactor(AxesUnit::Feet), kFeetPerMeter);
}
// 不显示模式 → 返回 nullptr不入场景
TEST(AxesActor, NoneModeReturnsNull) {
double b[6] = {0, 10, 0, 20, -5, 0};
AxesOptions opts;
opts.mode = AxesMode::None;
EXPECT_EQ(buildAxes(b, opts, nullptr), nullptr);
}
// 退化包围盒(全 0→ nullptr。
TEST(AxesActor, DegenerateBoundsReturnsNull) {
double zero[6] = {0, 0, 0, 0, 0, 0};
AxesOptions opts;
opts.mode = AxesMode::Standard;
EXPECT_EQ(buildAxes(zero, opts, nullptr), nullptr);
double inverted[6] = {10, 0, 0, 20, -5, 0}; // xmin>xmax
EXPECT_EQ(buildAxes(inverted, opts, nullptr), nullptr);
}
// 标准模式 + 米:构建非空,几何 bounds 保留X 显示范围 = 原值。
TEST(AxesActor, StandardMeterKeepsRange) {
double b[6] = {0, 100, 0, 200, -50, 0};
AxesOptions opts;
opts.mode = AxesMode::Standard;
opts.unit = AxesUnit::Meter;
auto ax = buildAxes(b, opts, nullptr);
ASSERT_NE(ax, nullptr);
double xr[2];
ax->GetXAxisRange(xr);
EXPECT_NEAR(xr[0], 0.0, 1e-9);
EXPECT_NEAR(xr[1], 100.0, 1e-9);
// 几何 bounds 不变。
double gb[6];
ax->GetBounds(gb);
EXPECT_NEAR(gb[1], 100.0, 1e-9);
}
// 英尺:显示范围 = 米值 × 3.28084(几何 bounds 仍为米)。
TEST(AxesActor, FeetScalesDisplayRange) {
double b[6] = {0, 100, 0, 200, -50, 0};
AxesOptions opts;
opts.mode = AxesMode::Standard;
opts.unit = AxesUnit::Feet;
auto ax = buildAxes(b, opts, nullptr);
ASSERT_NE(ax, nullptr);
double xr[2];
ax->GetXAxisRange(xr);
EXPECT_NEAR(xr[1], 100.0 * kFeetPerMeter, 1e-6);
// 几何 bounds 仍是米,不被换算。
double gb[6];
ax->GetBounds(gb);
EXPECT_NEAR(gb[1], 100.0, 1e-9);
}
// 经纬度X 显示范围反算为经度(在 lon0 附近、随 +x 增大)。
TEST(AxesActor, LatLonUsesFrameReverse) {
geopro::core::GeoLocalFrame frame(22.5, 114.16);
double b[6] = {0, 1000, 0, 1000, -50, 0}; // 1km 范围
AxesOptions opts;
opts.mode = AxesMode::Standard;
opts.unit = AxesUnit::LatLon;
opts.frame = &frame;
auto ax = buildAxes(b, opts, nullptr);
ASSERT_NE(ax, nullptr);
double xr[2], yr[2];
ax->GetXAxisRange(xr);
ax->GetYAxisRange(yr);
// x=0 → lon0x=1000m → 略大于 lon0。
EXPECT_NEAR(xr[0], 114.16, 1e-9);
EXPECT_GT(xr[1], 114.16);
EXPECT_NEAR(yr[0], 22.5, 1e-9);
EXPECT_GT(yr[1], 22.5);
}
// 经纬度但无 frame → 退化为米(不反算,显示范围 = 原值)。
TEST(AxesActor, LatLonWithoutFrameFallsBackToMeter) {
double b[6] = {0, 100, 0, 200, -50, 0};
AxesOptions opts;
opts.mode = AxesMode::Standard;
opts.unit = AxesUnit::LatLon;
opts.frame = nullptr;
auto ax = buildAxes(b, opts, nullptr);
ASSERT_NE(ax, nullptr);
double xr[2];
ax->GetXAxisRange(xr);
EXPECT_NEAR(xr[1], 100.0, 1e-9); // 米回退
}

View File

@ -0,0 +1,152 @@
#include <gtest/gtest.h>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkConeSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
#include "CameraPreset.hpp"
using namespace geopro::render;
namespace {
// 造一个带包围盒的 renderer一个 cone actor使 ResetCamera 有内容可重定位。
vtkSmartPointer<vtkRenderer> rendererWithContent() {
auto cone = vtkSmartPointer<vtkConeSource>::New();
cone->SetCenter(0, 0, 0);
cone->SetHeight(2.0);
cone->SetRadius(1.0);
auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(cone->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
auto r = vtkSmartPointer<vtkRenderer>::New();
r->AddActor(actor);
return r;
}
// 相机的视线方向单位向量 = focalPoint - position归一化
void viewDir(vtkRenderer* r, double out[3]) {
auto* c = r->GetActiveCamera();
double p[3], f[3];
c->GetPosition(p);
c->GetFocalPoint(f);
double d[3] = {f[0] - p[0], f[1] - p[1], f[2] - p[2]};
double n = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
out[0] = d[0] / n; out[1] = d[1] / n; out[2] = d[2] / n;
}
} // namespace
// Top相机在焦点上方(pos.z>focal.z),视线朝 -ZviewUp=+Y。
TEST(CameraPreset, TopLooksDown) {
auto r = rendererWithContent();
applyView(r, ViewDir::Top);
auto* c = r->GetActiveCamera();
double p[3], f[3], up[3];
c->GetPosition(p); c->GetFocalPoint(f); c->GetViewUp(up);
EXPECT_GT(p[2], f[2]); // 相机在上方
double d[3]; viewDir(r, d);
EXPECT_NEAR(d[2], -1.0, 1e-6); // 视线向下
EXPECT_NEAR(up[1], 1.0, 1e-6); // 北朝上
}
// Bottom相机在焦点下方视线朝 +Z。
TEST(CameraPreset, BottomLooksUp) {
auto r = rendererWithContent();
applyView(r, ViewDir::Bottom);
auto* c = r->GetActiveCamera();
double p[3], f[3];
c->GetPosition(p); c->GetFocalPoint(f);
EXPECT_LT(p[2], f[2]);
double d[3]; viewDir(r, d);
EXPECT_NEAR(d[2], 1.0, 1e-6);
}
// Front相机在 -Y视线朝 +YviewUp=+Z。
TEST(CameraPreset, FrontLooksNorth) {
auto r = rendererWithContent();
applyView(r, ViewDir::Front);
auto* c = r->GetActiveCamera();
double p[3], f[3], up[3];
c->GetPosition(p); c->GetFocalPoint(f); c->GetViewUp(up);
EXPECT_LT(p[1], f[1]);
double d[3]; viewDir(r, d);
EXPECT_NEAR(d[1], 1.0, 1e-6);
EXPECT_NEAR(up[2], 1.0, 1e-6);
}
// Back相机在 +Y视线朝 -Y。
TEST(CameraPreset, BackLooksSouth) {
auto r = rendererWithContent();
applyView(r, ViewDir::Back);
double d[3]; viewDir(r, d);
EXPECT_NEAR(d[1], -1.0, 1e-6);
}
// Left相机在 -X视线朝 +X。
TEST(CameraPreset, LeftLooksEast) {
auto r = rendererWithContent();
applyView(r, ViewDir::Left);
auto* c = r->GetActiveCamera();
double p[3], f[3];
c->GetPosition(p); c->GetFocalPoint(f);
EXPECT_LT(p[0], f[0]);
double d[3]; viewDir(r, d);
EXPECT_NEAR(d[0], 1.0, 1e-6);
}
// Right相机在 +X视线朝 -X。
TEST(CameraPreset, RightLooksWest) {
auto r = rendererWithContent();
applyView(r, ViewDir::Right);
double d[3]; viewDir(r, d);
EXPECT_NEAR(d[0], -1.0, 1e-6);
}
// zoomBy(>1) 放大:透视下 vtkCamera::Zoom 收窄视角ViewAngle 变小→画面放大)。
TEST(CameraPreset, ZoomInNarrowsViewAngle) {
auto r = rendererWithContent();
applyFree3D(r);
auto* c = r->GetActiveCamera();
const double before = c->GetViewAngle();
zoomBy(r, 1.2);
EXPECT_LT(c->GetViewAngle(), before);
}
// zoomBy(<1) 缩小:透视下视角变宽(画面缩小)。
TEST(CameraPreset, ZoomOutWidensViewAngle) {
auto r = rendererWithContent();
applyFree3D(r);
auto* c = r->GetActiveCamera();
const double before = c->GetViewAngle();
zoomBy(r, 1.0 / 1.2);
EXPECT_GT(c->GetViewAngle(), before);
}
// 正交投影下 zoomBy 改 parallelScale放大缩小可视范围
TEST(CameraPreset, ZoomInOrthoReducesParallelScale) {
auto r = rendererWithContent();
applyView(r, ViewDir::Top); // Top 不改投影模式;显式打开正交
auto* c = r->GetActiveCamera();
c->ParallelProjectionOn();
r->ResetCamera();
const double before = c->GetParallelScale();
zoomBy(r, 2.0);
EXPECT_LT(c->GetParallelScale(), before);
}
// 空指针/非法 factor 安全。
TEST(CameraPreset, NullAndInvalidAreSafe) {
applyView(nullptr, ViewDir::Top);
zoomBy(nullptr, 1.2);
fitView(nullptr);
auto r = rendererWithContent();
const double before = r->GetActiveCamera()->GetDistance();
zoomBy(r, 0.0); // 非法 factor 忽略
zoomBy(r, -1.0);
EXPECT_DOUBLE_EQ(r->GetActiveCamera()->GetDistance(), before);
}