diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 729744e..079bfc3 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -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 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 diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index dba7705..a06b406 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include +#include + #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 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 currentAxes_; }; } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 5b0485c..7114494 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -34,8 +34,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -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(geopro::controller::AxesMode::Standard)); + axesModeCombo->addItem(QStringLiteral("坐标轴:三维立体"), + static_cast(geopro::controller::AxesMode::Stereo)); + axesModeCombo->addItem(QStringLiteral("坐标轴:不显示"), + static_cast(geopro::controller::AxesMode::None)); + // 刻度单位下拉。 + auto* axesUnitCombo = new QComboBox(); + axesUnitCombo->addItem(QStringLiteral("刻度:无"), + static_cast(geopro::controller::AxesUnit::None)); + axesUnitCombo->addItem(QStringLiteral("刻度:米"), + static_cast(geopro::controller::AxesUnit::Meter)); + axesUnitCombo->addItem(QStringLiteral("刻度:英尺"), + static_cast(geopro::controller::AxesUnit::Feet)); + axesUnitCombo->addItem(QStringLiteral("刻度:经纬度"), + static_cast(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(kVerticalExaggeration)); + veSlider->setFixedWidth(80); + auto* veValue = new QLabel(QStringLiteral("%1x").arg(static_cast(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(&QComboBox::currentIndexChanged), sceneCtrl, + [sceneCtrl, axesModeCombo](int) { + sceneCtrl->setAxesMode( + static_cast(axesModeCombo->currentData().toInt())); + }); + QObject::connect(axesUnitCombo, qOverload(&QComboBox::currentIndexChanged), sceneCtrl, + [sceneCtrl, axesUnitCombo](int) { + sceneCtrl->setAxesUnit( + static_cast(axesUnitCombo->currentData().toInt())); + }); + QObject::connect(veSlider, &QSlider::valueChanged, sceneCtrl, + [sceneCtrl, veValue](int v) { + veValue->setText(QStringLiteral("%1x").arg(v)); + sceneCtrl->setVerticalExaggeration(static_cast(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, diff --git a/src/controller/I3dSceneView.hpp b/src/controller/I3dSceneView.hpp index 0e1e0d6..f745308 100644 --- a/src/controller/I3dSceneView.hpp +++ b/src/controller/I3dSceneView.hpp @@ -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; }; diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp index f0d5d97..20206e2 100644 --- a/src/controller/VtkSceneController.cpp +++ b/src/controller/VtkSceneController.cpp @@ -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 但不中断, diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp index 76532d3..4378732 100644 --- a/src/controller/VtkSceneController.hpp +++ b/src/controller/VtkSceneController.hpp @@ -6,6 +6,7 @@ #include #include +#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 gridCache_; std::map colorScaleCache_; diff --git a/src/core/geo/GeoLocalFrame.cpp b/src/core/geo/GeoLocalFrame.cpp index 18d3ec8..0b77b38 100644 --- a/src/core/geo/GeoLocalFrame.cpp +++ b/src/core/geo/GeoLocalFrame.cpp @@ -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 diff --git a/src/core/geo/GeoLocalFrame.hpp b/src/core/geo/GeoLocalFrame.hpp index e401352..ceec27b 100644 --- a/src/core/geo/GeoLocalFrame.hpp +++ b/src/core/geo/GeoLocalFrame.hpp @@ -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_; }; diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 0d25920..81a4066 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -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) diff --git a/src/render/CameraPreset.cpp b/src/render/CameraPreset.cpp index 70b809c..533cbc6 100644 --- a/src/render/CameraPreset.cpp +++ b/src/render/CameraPreset.cpp @@ -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 diff --git a/src/render/CameraPreset.hpp b/src/render/CameraPreset.hpp index 008ed30..99bfa1a 100644 --- a/src/render/CameraPreset.hpp +++ b/src/render/CameraPreset.hpp @@ -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 diff --git a/src/render/actors/AxesActor.cpp b/src/render/actors/AxesActor.cpp new file mode 100644 index 0000000..907f58f --- /dev/null +++ b/src/render/actors/AxesActor.cpp @@ -0,0 +1,98 @@ +#include "actors/AxesActor.hpp" + +#include +#include +#include + +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 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::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 diff --git a/src/render/actors/AxesActor.hpp b/src/render/actors/AxesActor.hpp new file mode 100644 index 0000000..4d35e3f --- /dev/null +++ b/src/render/actors/AxesActor.hpp @@ -0,0 +1,44 @@ +#pragma once +#include + +#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 buildAxes(const double bounds[6], const AxesOptions& opts, + vtkRenderer* renderer); + +// 单位换算系数(米→目标单位)。LatLon 不是线性系数(X/Y 分别反算),此处仅供米/英尺; +// 暴露为可测纯函数。 +double unitScaleFactor(AxesUnit unit); + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f855bc5..308a9b1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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}) diff --git a/tests/controller/test_vtk_scene_controller.cpp b/tests/controller/test_vtk_scene_controller.cpp index db8bf2c..66363ce 100644 --- a/tests/controller/test_vtk_scene_controller.cpp +++ b/tests/controller/test_vtk_scene_controller.cpp @@ -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); +} diff --git a/tests/core/test_geo_frame.cpp b/tests/core/test_geo_frame.cpp index 75a263a..a46e55c 100644 --- a/tests/core/test_geo_frame.cpp +++ b/tests/core/test_geo_frame.cpp @@ -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); +} diff --git a/tests/render/test_axes.cpp b/tests/render/test_axes.cpp new file mode 100644 index 0000000..e2087e5 --- /dev/null +++ b/tests/render/test_axes.cpp @@ -0,0 +1,105 @@ +#include + +#include + +#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); // 米回退 +} diff --git a/tests/render/test_camera_preset.cpp b/tests/render/test_camera_preset.cpp new file mode 100644 index 0000000..c563ee8 --- /dev/null +++ b/tests/render/test_camera_preset.cpp @@ -0,0 +1,152 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "CameraPreset.hpp" + +using namespace geopro::render; + +namespace { + +// 造一个带包围盒的 renderer(一个 cone actor),使 ResetCamera 有内容可重定位。 +vtkSmartPointer rendererWithContent() { + auto cone = vtkSmartPointer::New(); + cone->SetCenter(0, 0, 0); + cone->SetHeight(2.0); + cone->SetRadius(1.0); + auto mapper = vtkSmartPointer::New(); + mapper->SetInputConnection(cone->GetOutputPort()); + auto actor = vtkSmartPointer::New(); + actor->SetMapper(mapper); + auto r = vtkSmartPointer::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); +}