feat/vtk-3d-view #7
|
|
@ -3,6 +3,7 @@
|
|||
#include <utility>
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkCubeAxesActor.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkVolume.h>
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
#include "CameraPreset.hpp"
|
||||
#include "Scene.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "actors/AxesActor.hpp"
|
||||
#include "actors/CurtainActor.hpp"
|
||||
#include "actors/MapLineActor.hpp"
|
||||
#include "actors/TerrainActor.hpp"
|
||||
|
|
@ -18,6 +20,38 @@
|
|||
|
||||
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,
|
||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double zRefElev)
|
||||
: scene_(scene),
|
||||
|
|
@ -25,7 +59,10 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render
|
|||
frame_(std::move(frame)),
|
||||
zRefElev_(zRefElev) {}
|
||||
|
||||
void VtkSceneView::clear() { scene_.clear(); }
|
||||
void VtkSceneView::clear() {
|
||||
scene_.clear(); // RemoveAllViewProps:连同坐标轴一并移除
|
||||
currentAxes_ = nullptr; // 旧坐标轴已随 clear 移除,置空避免悬留引用
|
||||
}
|
||||
|
||||
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
|
||||
|
||||
|
|
@ -56,11 +93,58 @@ void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
|
|||
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() {
|
||||
// 先移除上一次的坐标轴 prop:render 可能在一次 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) {
|
||||
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
|
||||
double bgR, bgG, bgB;
|
||||
geopro::app::vtkBackground(bgR, bgG, bgB);
|
||||
scene_.renderer()->SetBackground(bgR, bgG, bgB);
|
||||
// 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。
|
||||
if (!is2D) rebuildAxes();
|
||||
if (is2D)
|
||||
geopro::render::applyTop2D(scene_.renderer());
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
#include <vtkCubeAxesActor.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
#include "I3dSceneView.hpp"
|
||||
|
||||
namespace geopro::core { class GeoLocalFrame; }
|
||||
|
|
@ -26,14 +29,30 @@ public:
|
|||
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 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;
|
||||
|
||||
private:
|
||||
// 按当前坐标轴设置 + 场景包围盒重建坐标轴 prop(render 末尾调)。
|
||||
void rebuildAxes();
|
||||
|
||||
geopro::render::Scene& scene_;
|
||||
vtkRenderWindow* renderWindow_;
|
||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame_;
|
||||
double zRefElev_;
|
||||
double verticalExaggeration_ = 2.0;
|
||||
|
||||
// 坐标轴设置(P2):默认标准 + 米。
|
||||
geopro::controller::AxesMode axesMode_ = geopro::controller::AxesMode::Standard;
|
||||
geopro::controller::AxesUnit axesUnit_ = geopro::controller::AxesUnit::Meter;
|
||||
int axesFontSize_ = 12;
|
||||
// 当前坐标轴 prop:render 可能多次调用 rebuildAxes(rebuild 末尾 + 异步回灌),
|
||||
// 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。
|
||||
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
154
src/app/main.cpp
154
src/app/main.cpp
|
|
@ -34,8 +34,11 @@
|
|||
#include <QFile>
|
||||
#include <QButtonGroup>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QDate>
|
||||
#include <QLabel>
|
||||
|
|
@ -181,6 +184,34 @@ private:
|
|||
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 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
||||
std::string readPem(const std::string& path)
|
||||
{
|
||||
|
|
@ -365,6 +396,80 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
layerLayout->addWidget(chkTerrain);
|
||||
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
||||
|
||||
// ──「三维数据集栏」工具条浮层(P2,spec §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); // 默认米(与控制器默认一致)
|
||||
// 纵向比例滑块(范围 1–10,默认 2;spec §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 时,引导首次使用者从左侧入手。──
|
||||
// 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中;
|
||||
// 接入真实中央数据后改成依 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) {
|
||||
layerPanel->move(14, viewHeader->height() + 12);
|
||||
layerPanel->adjustSize();
|
||||
layerPanel->setVisible(true);
|
||||
layerPanel->raise();
|
||||
axisBar->adjustSize();
|
||||
// 右上对齐:紧贴右边距 14px。
|
||||
axisBar->move(centerWidget->width() - axisBar->width() - 14, viewHeader->height() + 12);
|
||||
axisBar->setVisible(true);
|
||||
axisBar->raise();
|
||||
} else {
|
||||
layerPanel->setVisible(false);
|
||||
axisBar->setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -599,6 +711,44 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget,
|
||||
[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",空 → 清场)──
|
||||
// 真实接 Api 时改为把勾选 TM 映射到其 ds 维度过滤后的真实 dsId 列表(spec §6.1/§8)。
|
||||
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, sceneCtrl,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@
|
|||
|
||||
namespace geopro::controller {
|
||||
|
||||
// 坐标轴显示方式(spec §4 C3–I3):标准 / 三维立体 / 不显示。
|
||||
enum class AxesMode { Standard, Stereo, None };
|
||||
// 坐标轴刻度单位(spec §4 D5–I5):无 / 米 / 英尺 / 经纬度。
|
||||
enum class AxesUnit { None, Meter, Feet, LatLon };
|
||||
// 快捷视图方向(spec §4 C7):前/后/左/右/上/下。
|
||||
enum class ViewDir { Front, Back, Left, Right, Top, Bottom };
|
||||
|
||||
// 三维场景视图抽象(编排层与 VTK 渲染解耦的缝):
|
||||
// VtkSceneController 只发出"清场 / 加某类图元 / 提交渲染"指令,不认 vtkActor/vtkVolume;
|
||||
// 真实实现(VtkSceneView)调 render actor + Scene;测试用 fake 记录调用断言编排。
|
||||
|
|
@ -27,6 +34,17 @@ public:
|
|||
// 3D:DEM 地形 + 影像纹理。
|
||||
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;
|
||||
// 缩放(P2):factor>1 放大、<1 缩小,提交渲染。
|
||||
virtual void zoom(double factor) = 0;
|
||||
// 适配全览(P2):ResetCamera 并提交渲染。
|
||||
virtual void fitView() = 0;
|
||||
|
||||
// 应用相机预设(2D 俯视 / 3D 自由)并提交渲染。
|
||||
virtual void render(bool is2D) = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,6 +42,22 @@ void VtkSceneController::setVerticalExaggeration(double ve) {
|
|||
|
||||
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) {
|
||||
auto it = gridCache_.find(dsId);
|
||||
if (it == gridCache_.end()) it = gridCache_.emplace(dsId, dsRepo_.loadGrid(dsId)).first;
|
||||
|
|
@ -61,6 +77,8 @@ void VtkSceneController::rebuildInternal() {
|
|||
|
||||
view_.clear();
|
||||
view_.setVerticalExaggeration(verticalExaggeration_);
|
||||
// 坐标轴设置在 clear 后下发:render 末尾据当前场景包围盒重建坐标轴 prop。
|
||||
view_.setAxes(axesMode_, axesUnit_, kAxesFontSize);
|
||||
|
||||
inRebuild_ = true;
|
||||
// 坏 dsId(loadGrid/loadColorScale 抛异常)= best-effort 跳过:emit loadFailed 但不中断,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "I3dSceneView.hpp"
|
||||
#include "model/ColorScale.hpp"
|
||||
#include "model/Field.hpp"
|
||||
#include "repo/I3dSceneRepository.hpp"
|
||||
|
|
@ -16,8 +17,6 @@ class IDatasetRepository;
|
|||
|
||||
namespace geopro::controller {
|
||||
|
||||
class I3dSceneView;
|
||||
|
||||
// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。
|
||||
enum class ViewMode { Map2D, View3D };
|
||||
|
||||
|
|
@ -42,6 +41,14 @@ public slots:
|
|||
void setVerticalExaggeration(double ve);
|
||||
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:
|
||||
void loadFailed(const QString& message);
|
||||
|
||||
|
|
@ -59,6 +66,11 @@ private:
|
|||
bool showTerrain_ = false;
|
||||
double verticalExaggeration_ = 2.0;
|
||||
|
||||
// 坐标轴设置(P2):默认标准 + 米;字号固定 12(字体设置待 1.0 确认)。
|
||||
AxesMode axesMode_ = AxesMode::Standard;
|
||||
AxesUnit axesUnit_ = AxesUnit::Meter;
|
||||
static constexpr int kAxesFontSize = 12;
|
||||
|
||||
// 缓存(按 dsId):避免重复读盘/插值。
|
||||
std::map<std::string, geopro::core::Grid> gridCache_;
|
||||
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
|
||||
|
|
|
|||
|
|
@ -21,4 +21,8 @@ LocalXY GeoLocalFrame::toLocal(double lat, double lon) const {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
namespace geopro::core {
|
||||
|
||||
struct LocalXY { double x, y; };
|
||||
struct LatLon { double lat, lon; };
|
||||
|
||||
// 等距圆柱(equirectangular)近似:把经纬度投到以(lat0,lon0)为原点的局部米平面。
|
||||
// 小范围测区足够;x=East、y=North(米)。
|
||||
|
|
@ -9,6 +10,9 @@ class GeoLocalFrame {
|
|||
public:
|
||||
GeoLocalFrame(double lat0, double lon0);
|
||||
LocalXY toLocal(double lat, double lon) const; // -> (x East m, y North m)
|
||||
// toLocal 的反算:局部米 (x East, y North) -> 经纬度。
|
||||
// lon = lon0 + x/mPerDegLon,lat = lat0 + y/mPerDegLat(坐标轴经纬度刻度用)。
|
||||
LatLon toLatLon(double x, double y) const;
|
||||
private:
|
||||
double lat0_, lon0_, mPerDegLon_, mPerDegLat_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||
|
|
|
|||
|
|
@ -37,4 +37,54 @@ void applyFree3D(vtkRenderer* r)
|
|||
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
|
||||
|
|
|
|||
|
|
@ -8,4 +8,20 @@ void applyTop2D(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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
#include "geo/GeoLocalFrame.hpp"
|
||||
|
||||
class vtkCubeAxesActor;
|
||||
class vtkRenderer;
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
// 坐标轴显示方式(spec §4 C3–I3)。
|
||||
// Standard 标准 = vtkCubeAxesActor 包围盒 + 刻度(外侧最近轴显示刻度)。
|
||||
// Stereo 三维立体 = vtkCubeAxesActor 闭合立方(四周/网格更完整)。语义待 1.0 确认,先合理近似。
|
||||
// None 不显示 = 不构建(返回 nullptr)。
|
||||
enum class AxesMode { Standard, Stereo, None };
|
||||
|
||||
// 刻度单位(spec §4 D5–I5)。
|
||||
// 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。
|
||||
// camera:vtkCubeAxesActor 需绑定相机(决定外侧刻度轴);可空(测试场景)。
|
||||
vtkSmartPointer<vtkCubeAxesActor> buildAxes(const double bounds[6], const AxesOptions& opts,
|
||||
vtkRenderer* renderer);
|
||||
|
||||
// 单位换算系数(米→目标单位)。LatLon 不是线性系数(X/Y 分别反算),此处仅供米/英尺;
|
||||
// 暴露为可测纯函数。
|
||||
double unitScaleFactor(AxesUnit unit);
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -77,7 +77,7 @@ endif()
|
|||
|
||||
# render 层:ColorLutBuilder(core ColorScale -> vtkLookupTable)。
|
||||
# 需 vtkLookupTable(VTK::CommonCore);geopro_render 已 PUBLIC 传递其余 VTK 组件。
|
||||
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore)
|
||||
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore RenderingAnnotation FiltersSources)
|
||||
# Scene:addActor/addViewProp 计数 + clear 清空(vtkVolume 经 addViewProp 进场)。
|
||||
target_sources(geopro_tests PRIVATE render/test_scene.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)
|
||||
# Terrain:buildTerrain(GDAL 读 dem/image + 重投影 → warp 面+纹理) 非空/缺文件安全(需 PROJ_DATA)。
|
||||
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})
|
||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ struct FakeView : I3dSceneView {
|
|||
bool lastIs2D = false;
|
||||
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 累加。
|
||||
void clear() override {
|
||||
++clears;
|
||||
|
|
@ -37,6 +48,13 @@ struct FakeView : I3dSceneView {
|
|||
void addCurtain(const core::Grid&, const core::ColorScale&) override { ++curtains; }
|
||||
void addVolume(const data::VolumeGrid&, const core::ColorScale&) override { ++volumes; }
|
||||
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; }
|
||||
|
||||
int props() const { return surveyLines + curtains + volumes + terrains; }
|
||||
|
|
@ -165,3 +183,67 @@ TEST(VtkSceneController, MultipleDatasetsAddMultipleCurtains) {
|
|||
c.setCheckedDatasets({"ds1", "ds2", "ds3"});
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,3 +37,25 @@ TEST(GeoFrame, NorthwardLatitudeGivesPositiveY) {
|
|||
EXPECT_NEAR(p.y, expected, expected * 0.05);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 → lon0;x=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); // 米回退
|
||||
}
|
||||
|
|
@ -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),视线朝 -Z,viewUp=+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,视线朝 +Y,viewUp=+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);
|
||||
}
|
||||
Loading…
Reference in New Issue