diff --git a/build.bat b/build.bat index 86db412..08ffc6e 100644 --- a/build.bat +++ b/build.bat @@ -25,14 +25,17 @@ if not exist "%VSWHERE%" ( echo [build] vswhere not found. Open "x64 Native Tools Command Prompt for VS" and build manually. exit /b 1 ) -for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -property installationPath`) do set "VSPATH=%%i" -if not defined VSPATH ( echo [build] Visual Studio not found. & exit /b 1 ) +REM -all -prerelease for VS2026 preview (note: -latest yields empty on this preview, and +REM -products * would pull in the bundled BuildTools whose vcpkg/env breaks our preset); +REM -requires ensures the C++ toolset is present. Multiple installs -> last one wins. +for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -all -prerelease -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i" +if not defined VSPATH ( echo [build] Visual Studio with C++ toolset not found. Install the VS Desktop C++ workload. & exit /b 1 ) set "VCVARS=%VSPATH%\VC\Auxiliary\Build\vcvars64.bat" set "CMAKE=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" set "CTEST=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\ctest.exe" -if not exist "%VCVARS%" ( echo [build] vcvars64.bat not found: %VCVARS% & exit /b 1 ) -if not exist "%CMAKE%" ( echo [build] cmake not found: %CMAKE% & exit /b 1 ) +if not exist "%VCVARS%" ( echo [build] vcvars64.bat not found: "%VCVARS%" & exit /b 1 ) +if not exist "%CMAKE%" ( echo [build] cmake not found: "%CMAKE%" & exit /b 1 ) REM --- activate MSVC environment (cl / link / include / lib) --- call "%VCVARS%" >nul diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 4190b53..4f3f8fd 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -54,7 +54,7 @@ add_executable(geopro_desktop WIN32 panels/LoadingOverlay.cpp panels/DatasetDetailPage.cpp panels/DatasetDetailPanel.cpp - CentralScene.cpp + VtkSceneView.cpp ProjectListDialog.cpp ObjectFormDialog.cpp ImportDatasetDialog.cpp diff --git a/src/app/CentralScene.cpp b/src/app/CentralScene.cpp deleted file mode 100644 index faefd85..0000000 --- a/src/app/CentralScene.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "CentralScene.hpp" - -#include -#include -#include - -#include "CameraPreset.hpp" -#include "Scene.hpp" -#include "Theme.hpp" -#include "actors/CurtainActor.hpp" -#include "actors/MapLineActor.hpp" -#include "geo/GeoLocalFrame.hpp" - -namespace geopro::app { - -void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer, - vtkRenderWindow* renderWindow, ViewMode mode, - const std::vector& sections, bool showCurtain, - const geopro::core::GeoLocalFrame& frame, double verticalExaggeration) { - scene.clear(); - const bool is2D = (mode == ViewMode::Map2D); - (void)is2D; - // 背景永远深色(规范§0.5 视图区常深,不随明暗切换),让色阶数据更突出。 - double bgR, bgG, bgB; - geopro::app::vtkBackground(bgR, bgG, bgB); - renderer->SetBackground(bgR, bgG, bgB); - - for (const auto& s : sections) { - if (is2D) { - auto line = geopro::render::buildSurveyLine(s.grid, frame); - if (line) scene.addActor(line); - } else if (showCurtain) { - auto curtain = geopro::render::buildCurtain(s.grid, s.colorScale, frame); - if (curtain) { - curtain->SetScale(1.0, 1.0, verticalExaggeration); // 纵向夸张成墙 - scene.addActor(curtain); - } - } - } - - if (is2D) - geopro::render::applyTop2D(renderer); - else - geopro::render::applyFree3D(renderer); - renderer->ResetCamera(); - renderWindow->Render(); -} - -} // namespace geopro::app diff --git a/src/app/CentralScene.hpp b/src/app/CentralScene.hpp deleted file mode 100644 index 1e75032..0000000 --- a/src/app/CentralScene.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include - -#include "model/ColorScale.hpp" -#include "model/Field.hpp" - -namespace geopro::core { class GeoLocalFrame; } -namespace geopro::render { class Scene; } -class vtkRenderer; -class vtkRenderWindow; - -namespace geopro::app { - -// 中央视图模式:二维地图(测线红线俯视)/ 三维视图(断面墙)。 -enum class ViewMode { Map2D, View3D }; - -// 一个待渲染剖面:grid(2D 测线 / 3D 帘面都用)+ colorScale(3D 帘面上色)。 -struct SectionInput { - geopro::core::Grid grid; - geopro::core::ColorScale colorScale; -}; - -// 中央场景重建(脱离对象树,按显式 sections 渲染): -// 2D = 每个 section 的 buildSurveyLine;3D = 每个 section 的 buildCurtain(受 showCurtain)。 -// 下一轮接真实 DS:构建 sections 后调用本函数即可,render 层零改动。 -void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer, - vtkRenderWindow* renderWindow, ViewMode mode, - const std::vector& sections, bool showCurtain, - const geopro::core::GeoLocalFrame& frame, double verticalExaggeration); - -} // namespace geopro::app diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp new file mode 100644 index 0000000..729744e --- /dev/null +++ b/src/app/VtkSceneView.cpp @@ -0,0 +1,72 @@ +#include "VtkSceneView.hpp" + +#include + +#include +#include +#include +#include + +#include "CameraPreset.hpp" +#include "Scene.hpp" +#include "Theme.hpp" +#include "actors/CurtainActor.hpp" +#include "actors/MapLineActor.hpp" +#include "actors/TerrainActor.hpp" +#include "actors/VoxelActor.hpp" +#include "geo/GeoLocalFrame.hpp" + +namespace geopro::app { + +VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow, + std::shared_ptr frame, double zRefElev) + : scene_(scene), + renderWindow_(renderWindow), + frame_(std::move(frame)), + zRefElev_(zRefElev) {} + +void VtkSceneView::clear() { scene_.clear(); } + +void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; } + +void VtkSceneView::addSurveyLine(const geopro::core::Grid& grid) { + auto line = geopro::render::buildSurveyLine(grid, *frame_); + if (line) scene_.addActor(line); +} + +void VtkSceneView::addCurtain(const geopro::core::Grid& grid, const geopro::core::ColorScale& cs) { + auto curtain = geopro::render::buildCurtain(grid, cs, *frame_); + if (curtain) { + curtain->SetScale(1.0, 1.0, verticalExaggeration_); // 纵向夸张成墙 + scene_.addActor(curtain); + } +} + +void VtkSceneView::addVolume(const geopro::data::VolumeGrid& vol, const geopro::core::ColorScale& cs) { + // 纵向夸张烤进 image 的 z 原点/间距(与帘面 SetScale 同倍,保证纵向一致)。 + auto volume = geopro::render::buildVoxel( + vol.vol, cs, vol.origin[0], vol.origin[1], vol.origin[2] * verticalExaggeration_, + vol.spacing[0], vol.spacing[1], vol.spacing[2] * verticalExaggeration_, vol.vmin, vol.vmax); + if (volume) scene_.addViewProp(volume); +} + +void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) { + auto terrain = geopro::render::buildTerrain(paths.demPath, paths.imagePath, *frame_, zRefElev_, + verticalExaggeration_); + if (terrain) scene_.addActor(terrain); +} + +void VtkSceneView::render(bool is2D) { + // 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。 + double bgR, bgG, bgB; + geopro::app::vtkBackground(bgR, bgG, bgB); + scene_.renderer()->SetBackground(bgR, bgG, bgB); + if (is2D) + geopro::render::applyTop2D(scene_.renderer()); + else + geopro::render::applyFree3D(scene_.renderer()); + scene_.renderer()->ResetCamera(); + if (renderWindow_) renderWindow_->Render(); +} + +} // namespace geopro::app diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp new file mode 100644 index 0000000..dba7705 --- /dev/null +++ b/src/app/VtkSceneView.hpp @@ -0,0 +1,39 @@ +#pragma once +#include + +#include "I3dSceneView.hpp" + +namespace geopro::core { class GeoLocalFrame; } +namespace geopro::render { class Scene; } +class vtkRenderer; +class vtkRenderWindow; + +namespace geopro::app { + +// I3dSceneView 的真实实现:把编排层的"加图元"指令翻译为 render actor + Scene 调用。 +// 持有 Scene / renderer / renderWindow(非拥有)+ 共享 GeoLocalFrame(多视图空间配准)。 +// 纵向夸张统一作用:帘面/地形 actor SetScale(1,1,VE),体素 z 原点/间距烤入 VE。 +// render 层零业务:actor 只吃 core::*,本类负责装配。 +class VtkSceneView : public geopro::controller::I3dSceneView { +public: + // 入参生命周期须覆盖本对象(由调用方保证)。zRefElev:地形 z 基准(测线地表高程)。 + VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow, + std::shared_ptr frame, double zRefElev); + + void clear() override; + void setVerticalExaggeration(double ve) override; + void addSurveyLine(const geopro::core::Grid& grid) 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 addTerrain(const geopro::data::TerrainPaths& paths) override; + void render(bool is2D) override; + +private: + geopro::render::Scene& scene_; + vtkRenderWindow* renderWindow_; + std::shared_ptr frame_; + double zRefElev_; + double verticalExaggeration_ = 2.0; +}; + +} // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 94618f8..2eb4069 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -78,6 +78,7 @@ #include "model/ColorScale.hpp" #include "model/Field.hpp" #include "repo/LocalSampleRepository.hpp" +#include "repo/LocalSample3dRepository.hpp" #include "ApiClient.hpp" #include "AuthService.hpp" @@ -88,11 +89,12 @@ #include "Theme.hpp" #include "SettingsDialog.hpp" #include "TopBar.hpp" -#include "CentralScene.hpp" #include "ProjectListDialog.hpp" #include "ObjectFormDialog.hpp" #include "ImportDatasetDialog.hpp" #include "WorkbenchNavController.hpp" +#include "VtkSceneController.hpp" +#include "VtkSceneView.hpp" #include "api/NavRequest.hpp" #include "api/NavLoads.hpp" #include "DatasetDetailController.hpp" @@ -138,7 +140,6 @@ #include #include #include -#include #include #include #include @@ -199,9 +200,6 @@ double median(std::vector v) return n % 2 ? v[n / 2] : 0.5 * (v[n / 2 - 1] + v[n / 2]); } -// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。 -using geopro::app::ViewMode; - // 纵向夸张倍数(Z 基准统一,M-3):全项目共用同一倍数,使 帘面(z) / 体素 / 切片 / // 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。 // 单一可调常量:要整体调纵向观感改这一处即可。 @@ -231,29 +229,36 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // Scene 非 QObject:堆分配,用 widget 销毁信号清理(widget 随 window 销毁)。 auto* scene = new geopro::render::Scene(); auto* vtkWidget = new QVTKOpenGLStereoWidget(); - QObject::connect(vtkWidget, &QObject::destroyed, [scene]() { delete scene; }); vtkNew renderWindow; vtkWidget->setRenderWindow(renderWindow); renderWindow->AddRenderer(scene->renderer()); - vtkRenderer* rendererPtr = scene->renderer(); vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get(); - // 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。 - auto viewMode = std::make_shared(ViewMode::Map2D); + // 中央渲染编排(VtkSceneController + VtkSceneView,取代旧 rebuildCentral lambda 与裸 show* 标志)。 + // 3D 场景仓储用 LocalSample3dRepository(本期样本驱动;接口异步,将来换 Api 实现不动上层)。 + // 视图(VtkSceneView)非 QObject、控制器/3D 仓储亦然:随 scene 一并在 vtkWidget 销毁时清理。 + auto* scene3dRepo = new geopro::data::LocalSample3dRepository(repo, kProjectCrs, lat0, lon0); + auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr, + frame, refElev); + auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView, + vtkWidget); + sceneCtrl->setVerticalExaggeration(kVerticalExaggeration); + // 非 QObject 堆对象统一在此清理,按构造逆序:sceneView(持 scene&) → scene3dRepo → scene。 + // (sceneCtrl 是 vtkWidget 的 QObject 子对象,由 Qt 在 destroyed 前先析构,不再触发信号回灌。) + QObject::connect(vtkWidget, &QObject::destroyed, [scene, scene3dRepo, sceneView]() { + delete sceneView; + delete scene3dRepo; + delete scene; + }); - // 三维图层显隐(由「视图详情」浮层控制)+ 项目 CRS→WGS84(体素配准)。 - auto showCurtain = std::make_shared(true); // 帘面,默认显示 - auto showVoxel = std::make_shared(false); // 体素,默认关 - auto showTerrain = std::make_shared(false); // 地形(DEM+影像),默认关 - auto showSlice = std::make_shared(false); // dd_slice 交互切片,默认关 - // 持久的切片 widget(挂 interactor,跨重建保活;rebuildCentral 据条件创建/拆除)。 - auto slicePlane = std::make_shared>(); - std::shared_ptr crs; // PROJ 失败→空→体素层无效(不崩) + // PROJ 可用性(体素/地形/切片层都需配准):失败则浮层相应勾选禁用并提示。 + bool crsAvailable = false; try { - crs = std::make_shared(kProjectCrs, kWgs84); + geopro::core::CrsTransform probe(kProjectCrs, kWgs84); + crsAvailable = true; } catch (const std::exception&) { - crs.reset(); + crsAvailable = false; } // 停靠系统配置(必须在 CDockManager 构造前设置):对齐原型——面板固定、 @@ -345,17 +350,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re chkTerrain->setChecked(false); auto* chkSlice = new QCheckBox(QStringLiteral("切片(dd_slice)")); chkSlice->setChecked(false); - if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示 + if (!crsAvailable) { // PROJ 不可用 → 体素/地形层(都需配准)禁用并提示 const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用"); chkVoxel->setEnabled(false); chkVoxel->setToolTip(tip); chkTerrain->setEnabled(false); chkTerrain->setToolTip(tip); - chkSlice->setEnabled(false); chkSlice->setToolTip(tip); - } - // 本轮中央不接真实派生层:体素/切片/地形勾选置灰,待下一轮接入对应数据源。 - for (QCheckBox* c : {chkVoxel, chkSlice, chkTerrain}) { - c->setEnabled(false); - c->setToolTip(QStringLiteral("(下一轮接入真实数据源)")); } + // 切片(dd_slice)交互切片留待 P3:本轮禁用。 + chkSlice->setEnabled(false); + chkSlice->setToolTip(QStringLiteral("(切片交互 P3 接入)")); layerLayout->addWidget(layerTitle); layerLayout->addWidget(chkCurtain); layerLayout->addWidget(chkVoxel); @@ -503,13 +505,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }; hideDockTitleBars(); - // 中央编排已解耦到 CentralScene::rebuildCentralScene(数据驱动)。本轮空 sections → 空背景占位。 - // 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。 - auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, showCurtain, frame]() { - geopro::app::rebuildCentralScene(*scene, rendererPtr, renderWindowPtr, *viewMode, - std::vector{}, *showCurtain, - *frame, kVerticalExaggeration); - }; + // 中央渲染由 sceneCtrl(VtkSceneController)驱动:勾选对象/2D-3D切换/图层勾选/主题 → 重建场景。 + // (旧 rebuildCentral lambda + 裸 show* 标志已由控制器取代。) // ── 单击左下数据列表的采集批次(DS) → 属性表单 + 聚焦详情已开页 ── QObject::connect(datasetList, &QTreeWidget::itemClicked, datasetList, @@ -580,51 +577,44 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } }; - // ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ── + // ── 工具条「二维地图/三维视图」:切换互斥视图 → 控制器重建 + 图层浮层显隐 ── + using geopro::controller::SceneLayer; + using CtrlViewMode = geopro::controller::ViewMode; QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget, - [viewMode, rebuildCentral, showLayerPanel]() { - *viewMode = ViewMode::Map2D; + [sceneCtrl, showLayerPanel]() { showLayerPanel(false); - rebuildCentral(); + sceneCtrl->setViewMode(CtrlViewMode::Map2D); }); QObject::connect(act3D, &QAbstractButton::clicked, vtkWidget, - [viewMode, rebuildCentral, showLayerPanel]() { - *viewMode = ViewMode::View3D; + [sceneCtrl, showLayerPanel]() { showLayerPanel(true); - rebuildCentral(); + sceneCtrl->setViewMode(CtrlViewMode::View3D); }); - // ──「视图详情」图层勾选 → 更新图层显隐 → 重建中央 ── + // ──「视图详情」图层勾选 → 控制器更新图层 → 重建中央 ── QObject::connect(chkCurtain, &QCheckBox::toggled, vtkWidget, - [showCurtain, rebuildCentral](bool on) { - *showCurtain = on; - rebuildCentral(); - }); + [sceneCtrl](bool on) { sceneCtrl->setLayer(SceneLayer::Curtain, on); }); QObject::connect(chkVoxel, &QCheckBox::toggled, vtkWidget, - [showVoxel, rebuildCentral](bool on) { - *showVoxel = on; - rebuildCentral(); - }); + [sceneCtrl](bool on) { sceneCtrl->setLayer(SceneLayer::Voxel, on); }); QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget, - [showTerrain, rebuildCentral](bool on) { - *showTerrain = on; - rebuildCentral(); - }); - QObject::connect(chkSlice, &QCheckBox::toggled, vtkWidget, - [showSlice, rebuildCentral](bool on) { - *showSlice = on; - rebuildCentral(); + [sceneCtrl](bool on) { sceneCtrl->setLayer(SceneLayer::Terrain, on); }); + + // ── 左上对象树勾选 → 渲染勾选数据集(本期样本驱动:任意勾选 → 样本 ds "grid1",空 → 清场)── + // 真实接 Api 时改为把勾选 TM 映射到其 ds 维度过滤后的真实 dsId 列表(spec §6.1/§8)。 + QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, sceneCtrl, + [sceneCtrl](const QStringList& tmIds) { + sceneCtrl->setCheckedDatasets(tmIds.isEmpty() ? QStringList{} + : QStringList{QStringLiteral("grid1")}); }); - // ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。 - rebuildCentral(); + // ── 启动:建立一次中央视图(默认 2D,无勾选 → 空场景 + 背景)。 + sceneCtrl->setViewMode(CtrlViewMode::Map2D); - // VTK 背景随主题切换:直接重跑 rebuildCentral(走完整渲染路径、末尾必 Render, - // 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。 - QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, &window, - [rebuildCentral]() { - rebuildCentral(); - }); + // VTK 背景随主题切换:控制器重渲染(走完整渲染路径、末尾必 Render)。 + // context 用 sceneCtrl(非 window):ThemeManager 是进程级单例,连接须随 sceneCtrl 析构自动断开, + // 否则 window 析构期间 sceneCtrl(其孙级子对象)已销毁、主题异步变化会触悬垂指针。 + QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, sceneCtrl, + [sceneCtrl]() { sceneCtrl->rebuild(); }); // 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备), // 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。 diff --git a/src/controller/CMakeLists.txt b/src/controller/CMakeLists.txt index 0bbe490..54a5a11 100644 --- a/src/controller/CMakeLists.txt +++ b/src/controller/CMakeLists.txt @@ -1,7 +1,8 @@ find_package(Qt6 COMPONENTS Core REQUIRED) add_library(geopro_controller STATIC WorkbenchNavController.cpp - DatasetDetailController.cpp) + DatasetDetailController.cpp + VtkSceneController.cpp) target_include_directories(geopro_controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_controller PUBLIC geopro_data Qt6::Core) target_compile_features(geopro_controller PUBLIC cxx_std_17) diff --git a/src/controller/I3dSceneView.hpp b/src/controller/I3dSceneView.hpp new file mode 100644 index 0000000..0e1e0d6 --- /dev/null +++ b/src/controller/I3dSceneView.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "model/ColorScale.hpp" +#include "model/Field.hpp" +#include "repo/I3dSceneRepository.hpp" + +namespace geopro::controller { + +// 三维场景视图抽象(编排层与 VTK 渲染解耦的缝): +// VtkSceneController 只发出"清场 / 加某类图元 / 提交渲染"指令,不认 vtkActor/vtkVolume; +// 真实实现(VtkSceneView)调 render actor + Scene;测试用 fake 记录调用断言编排。 +// verticalExaggeration 由视图统一作用于 3D 图元(actor SetScale(1,1,VE) / image z 烤入)。 +class I3dSceneView { +public: + virtual ~I3dSceneView() = default; + + virtual void clear() = 0; + virtual void setVerticalExaggeration(double ve) = 0; + + // 2D:俯视测线红线(z=0)。 + virtual void addSurveyLine(const geopro::core::Grid& grid) = 0; + // 3D:竖直帘面(grid + colorScale 着色)。 + virtual void addCurtain(const geopro::core::Grid& grid, + const geopro::core::ColorScale& cs) = 0; + // 3D:体绘制(IDW 体素 + colorScale)。 + virtual void addVolume(const geopro::data::VolumeGrid& vol, + const geopro::core::ColorScale& cs) = 0; + // 3D:DEM 地形 + 影像纹理。 + virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0; + + // 应用相机预设(2D 俯视 / 3D 自由)并提交渲染。 + virtual void render(bool is2D) = 0; +}; + +} // namespace geopro::controller diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp new file mode 100644 index 0000000..f0d5d97 --- /dev/null +++ b/src/controller/VtkSceneController.cpp @@ -0,0 +1,121 @@ +#include "VtkSceneController.hpp" + +#include + +#include + +#include "I3dSceneView.hpp" +#include "repo/IDatasetRepository.hpp" + +namespace geopro::controller { + +VtkSceneController::VtkSceneController(data::IDatasetRepository& dsRepo, + data::I3dSceneRepository& sceneRepo, I3dSceneView& view, + QObject* parent) + : QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {} + +void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) { + checkedDs_.clear(); + checkedDs_.reserve(static_cast(dsIds.size())); + for (const QString& id : dsIds) checkedDs_.push_back(id.toStdString()); + rebuildInternal(); +} + +void VtkSceneController::setViewMode(ViewMode mode) { + mode_ = mode; + rebuildInternal(); +} + +void VtkSceneController::setLayer(SceneLayer layer, bool on) { + switch (layer) { + case SceneLayer::Curtain: showCurtain_ = on; break; + case SceneLayer::Voxel: showVoxel_ = on; break; + case SceneLayer::Terrain: showTerrain_ = on; break; + } + rebuildInternal(); +} + +void VtkSceneController::setVerticalExaggeration(double ve) { + verticalExaggeration_ = ve; + rebuildInternal(); +} + +void VtkSceneController::rebuild() { rebuildInternal(); } + +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; + return it->second; +} + +const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string& dsId) { + auto it = colorScaleCache_.find(dsId); + if (it == colorScaleCache_.end()) + it = colorScaleCache_.emplace(dsId, dsRepo_.loadColorScale(dsId)).first; + return it->second; +} + +void VtkSceneController::rebuildInternal() { + const unsigned long long gen = ++rebuildGeneration_; + const bool is2D = (mode_ == ViewMode::Map2D); + + view_.clear(); + view_.setVerticalExaggeration(verticalExaggeration_); + + inRebuild_ = true; + // 坏 dsId(loadGrid/loadColorScale 抛异常)= best-effort 跳过:emit loadFailed 但不中断, + // 其余勾选数据集照常渲染(非 fail-fast)。 + try { + if (is2D) { + for (const auto& dsId : checkedDs_) view_.addSurveyLine(grid(dsId)); + } else { + // 回调用 QPointer 守对象存活(控制器是 QObject)+ gen 守数据新鲜: + // 将来 Api 实现在网络线程迟到回调时,self 已析构则直接丢弃,不触 dangling。 + QPointer self(this); + if (showTerrain_) { + sceneRepo_.loadTerrainPaths( + [self, gen](data::TerrainPaths p) { + if (!self || gen != self->rebuildGeneration_) return; // 已析构/迟到:丢弃 + self->view_.addTerrain(std::move(p)); + if (!self->inRebuild_) self->view_.render(false); // 同步路径由末尾统一 render + }, + [self, gen](const std::string& m) { + if (!self || gen != self->rebuildGeneration_) return; + emit self->loadFailed(QString::fromStdString(m)); + }); + } + if (showCurtain_) { + for (const auto& dsId : checkedDs_) view_.addCurtain(grid(dsId), colorScale(dsId)); + } + if (showVoxel_) { + for (const auto& dsId : checkedDs_) { + auto cached = volumeCache_.find(dsId); + if (cached != volumeCache_.end()) { + view_.addVolume(cached->second, colorScale(dsId)); + continue; + } + sceneRepo_.loadVolume( + dsId, + [self, gen, dsId](data::VolumeGrid g) { + if (!self) return; // 控制器已析构:丢弃 + if (gen != self->rebuildGeneration_) return; // 迟到回灌:丢弃 + auto it = self->volumeCache_.emplace(dsId, std::move(g)).first; + self->view_.addVolume(it->second, self->colorScale(dsId)); + if (!self->inRebuild_) self->view_.render(false); // 同步路径由末尾统一 render + }, + [self, gen](const std::string& m) { + if (!self || gen != self->rebuildGeneration_) return; + emit self->loadFailed(QString::fromStdString(m)); + }); + } + } + } + } catch (const std::exception& e) { + emit loadFailed(QString::fromStdString(e.what())); + } + + inRebuild_ = false; + view_.render(is2D); +} + +} // namespace geopro::controller diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp new file mode 100644 index 0000000..76532d3 --- /dev/null +++ b/src/controller/VtkSceneController.hpp @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "model/ColorScale.hpp" +#include "model/Field.hpp" +#include "repo/I3dSceneRepository.hpp" + +namespace geopro::data { +class IDatasetRepository; +} + +namespace geopro::controller { + +class I3dSceneView; + +// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。 +enum class ViewMode { Map2D, View3D }; + +// 三维图层("视图详情"浮层勾选)。 +enum class SceneLayer { Curtain, Voxel, Terrain }; + +// 中央 VTK 渲染编排(spec §8):聚合 勾选数据集 + 视图模式 + 图层开关 + 纵向比例, +// 经仓储取 core::* 数据,命令 I3dSceneView 重建场景。取代 main.cpp 的 rebuildCentral lambda。 +// 异步:经 I3dSceneRepository 回调取体素/地形(回调内置幂请求标记防迟到回灌)。 +// 缓存:Grid / VolumeGrid 按 dsId 缓存,避免重复取数。 +// 不持有 widget;不认 vtkActor/vtkVolume(全交给 I3dSceneView)。 +class VtkSceneController : public QObject { + Q_OBJECT +public: + VtkSceneController(data::IDatasetRepository& dsRepo, data::I3dSceneRepository& sceneRepo, + I3dSceneView& view, QObject* parent = nullptr); + +public slots: + void setCheckedDatasets(const QStringList& dsIds); + void setViewMode(ViewMode mode); + void setLayer(SceneLayer layer, bool on); + void setVerticalExaggeration(double ve); + void rebuild(); // 主题切换等外部触发的重渲染 + +signals: + void loadFailed(const QString& message); + +private: + void rebuildInternal(); + + data::IDatasetRepository& dsRepo_; + data::I3dSceneRepository& sceneRepo_; + I3dSceneView& view_; + + std::vector checkedDs_; + ViewMode mode_ = ViewMode::Map2D; + bool showCurtain_ = true; + bool showVoxel_ = false; + bool showTerrain_ = false; + double verticalExaggeration_ = 2.0; + + // 缓存(按 dsId):避免重复读盘/插值。 + std::map gridCache_; + std::map colorScaleCache_; + std::map volumeCache_; + + // 异步回灌防护:每次 rebuild 自增,回调比对丢弃迟到结果。 + unsigned long long rebuildGeneration_ = 0; + // rebuild 进行中标志:同步回调(LocalSample)在 rebuild 内立即触发时跳过自身 render, + // 由 rebuildInternal 末尾统一 render 覆盖(避免双重 ResetCamera/Render); + // 真异步回调迟到时 inRebuild_ 已 false → 自行 render 追加。 + bool inRebuild_ = false; + + const geopro::core::Grid& grid(const std::string& dsId); + const geopro::core::ColorScale& colorScale(const std::string& dsId); +}; + +} // namespace geopro::controller diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index a192760..684633e 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -3,6 +3,7 @@ find_package(Qt6 COMPONENTS Core REQUIRED) add_library(geopro_data STATIC parse/SampleParsers.cpp repo/LocalSampleRepository.cpp + repo/LocalSample3dRepository.cpp dto/NavDto.cpp dto/DatasetChartDto.cpp dto/MeasurementDto.cpp diff --git a/src/data/repo/I3dSceneRepository.hpp b/src/data/repo/I3dSceneRepository.hpp new file mode 100644 index 0000000..9054aeb --- /dev/null +++ b/src/data/repo/I3dSceneRepository.hpp @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#include + +#include "model/Field.hpp" +#include "repo/RepoTypes.hpp" + +namespace geopro::data { + +// ds 维度属性(由 ds 类型/ddCode 决定,spec §6.1)。三栏列表筛选用。 +enum class DsDimension { Dim2D, Dim3D, Analysis3D, Other }; + +// 三维体模型数据:规则标量体 + 世界系原点/间距 + 值域(去裸 double[],用 std::array,spec §6.2)。 +struct VolumeGrid { + geopro::core::ScalarVolume vol{0, 0, 0}; + std::array origin{{0.0, 0.0, 0.0}}; // ox, oy, oz(世界米) + std::array spacing{{0.0, 0.0, 0.0}}; // dx, dy, dz + double vmin = 0.0, vmax = 0.0; + bool valid() const { return vol.nx() > 0 && vol.ny() > 0 && vol.nz() > 0 && vmax > vmin; } +}; + +// DEM/影像 GeoTIFF 绝对路径(供 render::buildTerrain 经 GDAL 读,spec §6.2)。 +struct TerrainPaths { + std::string demPath, imagePath; +}; + +// 三维场景仓储抽象(异步,spec §6 评审 HIGH)。 +// 取数方法走回调 std::function(LocalSample 本地数据同步算好后直接回调; +// 将来 Api3dRepository 在网络完成时回调,上层不变)。 +// **契约:onOk/onErr 必须在主(GUI)线程调用**——上层(VtkSceneController)回调内直接操作 +// 场景/发 Qt 信号,依赖主线程亲和;Api 实现若在工作线程完成须 post 回主线程再回调。 +// dimensionOf 是同步纯函数(无 I/O,只做类型→维度映射)。 +// 切片/异常/任务等签名本期不在接口内(留 P3/P4)。 +class I3dSceneRepository { +public: + using OnError = std::function; + + virtual ~I3dSceneRepository() = default; + + // 同步纯函数:ds 类型 → 维度(spec §6.1 映射表)。 + virtual DsDimension dimensionOf(const DsRow& ds) const = 0; + + // 异步:加载三维体模型(成功回调 VolumeGrid,失败回调消息)。 + virtual void loadVolume(const std::string& dsId, + std::function onOk, OnError onErr) = 0; + + // 异步:加载地形 DEM/影像路径。 + virtual void loadTerrainPaths(std::function onOk, OnError onErr) = 0; +}; + +} // namespace geopro::data diff --git a/src/data/repo/LocalSample3dRepository.cpp b/src/data/repo/LocalSample3dRepository.cpp new file mode 100644 index 0000000..795d295 --- /dev/null +++ b/src/data/repo/LocalSample3dRepository.cpp @@ -0,0 +1,159 @@ +#include "repo/LocalSample3dRepository.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "algo/IdwInterpolator.hpp" +#include "geo/CrsTransform.hpp" +#include "geo/GeoLocalFrame.hpp" +#include "model/ColorScale.hpp" +#include "model/Field.hpp" +#include "repo/LocalSampleRepository.hpp" + +namespace geopro::data { + +using geopro::core::ColorScale; +using geopro::core::CrsTransform; +using geopro::core::GeoLocalFrame; +using geopro::core::GridSpec; +using geopro::core::IdwInterpolator; +using geopro::core::PointSet; +using geopro::core::ScalarVolume; +using geopro::core::ScatterField; + +namespace { + +// 与 render::VoxelFromScatters 的默认参数同口径(保持渲染/切片纵向一致)。 +// TODO(P2/P3): 与 render::buildVoxelFromScatters 的 cellXY/cellZ/power/maxDist 默认值重复, +// 宜把"散点→配准→GridSpec→IDW→ScalarVolume"提到 core::algo 共享,避免单方调参静默不一致。 +constexpr double kCellXY = 1.0; +constexpr double kCellZ = 0.5; +constexpr double kPower = 2.0; +constexpr double kMaxDist = 4.0; +constexpr int kMaxDim = 400; +constexpr const char* kWgs84 = "EPSG:4326"; + +int clampDim(double ext, double cell) { + int n = static_cast(ext / cell) + 1; + if (n < 1) n = 1; + if (n > kMaxDim) n = kMaxDim; + return n; +} + +} // namespace + +LocalSample3dRepository::LocalSample3dRepository(LocalSampleRepository& base, std::string projectCrs, + double baseLat, double baseLon) + : base_(base), projectCrs_(std::move(projectCrs)), baseLat_(baseLat), baseLon_(baseLon) {} + +DsDimension LocalSample3dRepository::dimensionOf(const DsRow& ds) const { + const std::string& c = ds.ddCode; + // 真三维体 / 体素 / 帘面(dd_section/反演剖面摆成竖直帘面)入三维数据集。 + if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" || c == "dd_section" || + c == "dd_inversion_data") { + return DsDimension::Dim3D; + } + // 切片:三维分析栏。 + if (c == "dd_slice") return DsDimension::Analysis3D; + // 轨迹:二维数据集。 + if (c == "dd_trajectory_data") return DsDimension::Dim2D; + return DsDimension::Other; +} + +void LocalSample3dRepository::loadVolume(const std::string& /*dsId*/, + std::function onOk, OnError onErr) { + // P1 样本:dsId 暂未使用,固定读同一组交叉剖面散点→体素(真实 Api 实现按 dsId 取)。 + try { + // 1) 读两条交叉剖面散点 + 色阶;配准到世界局部米 + 深度,组装 IDW 输入点集。 + const std::vector profiles = base_.loadVoxelScatters(); + const CrsTransform crs(projectCrs_, kWgs84); + const GeoLocalFrame frame(baseLat_, baseLon_); + + PointSet pts; + for (const auto& s : profiles) { + const std::size_t n = s.v.size(); + if (s.projX.size() < n || s.projY.size() < n || s.y.size() < n) continue; + for (std::size_t i = 0; i < n; ++i) { + const auto ll = crs.forward(s.projX[i], s.projY[i]); // (lon, lat) + const auto local = frame.toLocal(ll.y, ll.x); // (x East, y North) 米 + pts.x.push_back(local.x); + pts.y.push_back(local.y); + pts.z.push_back(-s.y[i]); // 深度向下:z 取负 + pts.v.push_back(s.v[i]); + } + } + if (pts.v.empty()) { + onErr("LocalSample3dRepository: no voxel points after registration"); + return; + } + + // 2) 点集包络 → GridSpec(角点对齐)。 + double minx = pts.x[0], maxx = pts.x[0]; + double miny = pts.y[0], maxy = pts.y[0]; + double minz = pts.z[0], maxz = pts.z[0]; + for (std::size_t i = 1; i < pts.v.size(); ++i) { + minx = std::min(minx, pts.x[i]); maxx = std::max(maxx, pts.x[i]); + miny = std::min(miny, pts.y[i]); maxy = std::max(maxy, pts.y[i]); + minz = std::min(minz, pts.z[i]); maxz = std::max(maxz, pts.z[i]); + } + + GridSpec spec{}; + spec.ox = minx; spec.oy = miny; spec.oz = minz; + spec.dx = kCellXY; spec.dy = kCellXY; spec.dz = kCellZ; + spec.nx = clampDim(maxx - minx, kCellXY); + spec.ny = clampDim(maxy - miny, kCellXY); + spec.nz = clampDim(maxz - minz, kCellZ); + spec.power = kPower; + spec.maxDist = kMaxDist; + + // 3) IDW → ScalarVolume(maxDist 外 NaN 留空)。 + const IdwInterpolator idw; + ScalarVolume vol = idw.interpolate(pts, spec); + + // 4) 值域:优先 colorBar 真实分段值,否则数据实测。 + double vmin, vmax; + ColorScale cs; + try { + cs = base_.loadScatterColorScale("grid1"); + } catch (const std::exception&) { + // 色阶缺失 → 退化为数据实测范围。 + } + const std::vector stops = cs.stopValues(); + if (stops.size() >= 2) { + vmin = stops.front(); vmax = stops.back(); + } else { + vmin = std::numeric_limits::infinity(); + vmax = -std::numeric_limits::infinity(); + for (double v : vol.data()) { + if (std::isnan(v)) continue; + vmin = std::min(vmin, v); vmax = std::max(vmax, v); + } + if (!(vmin < vmax)) { vmin = 0.0; vmax = 1.0; } + } + + VolumeGrid out{std::move(vol), + {{spec.ox, spec.oy, spec.oz}}, + {{spec.dx, spec.dy, spec.dz}}, + vmin, vmax}; + onOk(std::move(out)); + } catch (const std::exception& e) { + onErr(std::string("LocalSample3dRepository::loadVolume: ") + e.what()); + } +} + +void LocalSample3dRepository::loadTerrainPaths(std::function onOk, + OnError onErr) { + try { + TerrainPaths p{base_.demPath(), base_.imagePath()}; + onOk(std::move(p)); + } catch (const std::exception& e) { + onErr(std::string("LocalSample3dRepository::loadTerrainPaths: ") + e.what()); + } +} + +} // namespace geopro::data diff --git a/src/data/repo/LocalSample3dRepository.hpp b/src/data/repo/LocalSample3dRepository.hpp new file mode 100644 index 0000000..22353b9 --- /dev/null +++ b/src/data/repo/LocalSample3dRepository.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include + +#include "repo/I3dSceneRepository.hpp" + +namespace geopro::data { + +class LocalSampleRepository; + +// 本地样本三维场景仓储(spec §6):组合 LocalSampleRepository。 +// loadVolume:读两条交叉剖面散点 → 项目 CRS→WGS84→GeoLocalFrame 配准 → IDW → VolumeGrid(纯 core/data,无 VTK)。 +// loadTerrainPaths:直透 LocalSampleRepository 的 demPath/imagePath。 +// dimensionOf:按 ddCode 内置映射表(同步纯函数)。 +// 本地数据同步算好后直接回调(异步壳:接口异步,本实现内联完成)。 +class LocalSample3dRepository : public I3dSceneRepository { +public: + // base 生命周期须覆盖本对象(由调用方保证);projectCrs 为项目 CRS(如 "EPSG:4547")。 + // baseLat/baseLon 为全项目共享 GeoLocalFrame 原点(与帘面/地图同系,保证空间配准)。 + LocalSample3dRepository(LocalSampleRepository& base, std::string projectCrs, + double baseLat, double baseLon); + + DsDimension dimensionOf(const DsRow& ds) const override; + void loadVolume(const std::string& dsId, std::function onOk, + OnError onErr) override; + void loadTerrainPaths(std::function onOk, OnError onErr) override; + +private: + LocalSampleRepository& base_; + std::string projectCrs_; + double baseLat_, baseLon_; +}; + +} // namespace geopro::data diff --git a/src/render/Scene.cpp b/src/render/Scene.cpp index f1d28b5..6181f06 100644 --- a/src/render/Scene.cpp +++ b/src/render/Scene.cpp @@ -17,4 +17,9 @@ void Scene::addActor(vtkActor* a) if (a) renderer_->AddActor(a); } +void Scene::addViewProp(vtkProp* p) +{ + if (p) renderer_->AddViewProp(p); +} + } // namespace geopro::render diff --git a/src/render/Scene.hpp b/src/render/Scene.hpp index 2cb49e3..9354243 100644 --- a/src/render/Scene.hpp +++ b/src/render/Scene.hpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace geopro::render { // 单一渲染场景:持有 vtkRenderer(白底),统一管理 actor 的加入与清除。 @@ -12,8 +13,10 @@ public: vtkRenderer* renderer() const { return renderer_.Get(); } - void clear(); // 移除所有 view prop,支持重复切换数据集 + void clear(); // 移除所有 view prop(含体绘制 vtkVolume),支持重复切换数据集 void addActor(vtkActor* a); // actor 由 renderer 引用计数保活 + // 体绘制 vtkVolume 是 vtkProp3D(非 vtkActor),经此通用入口进场;prop 由 renderer 引用计数保活。 + void addViewProp(vtkProp* p); private: vtkSmartPointer renderer_; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cf0a2d8..f855bc5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,6 +36,8 @@ target_link_libraries(geopro_tests PRIVATE geopro_core) target_sources(geopro_tests PRIVATE data/test_parsers.cpp) target_sources(geopro_tests PRIVATE data/test_local_repo.cpp) +# I3dSceneRepository/LocalSample3dRepository:dimensionOf 映射 + loadVolume/loadTerrainPaths 异步回调(需 PROJ_DATA)。 +target_sources(geopro_tests PRIVATE data/test_3d_repo.cpp) target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp) target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp) target_sources(geopro_tests PRIVATE data/test_measurement_dto.cpp) @@ -76,6 +78,8 @@ endif() # render 层:ColorLutBuilder(core ColorScale -> vtkLookupTable)。 # 需 vtkLookupTable(VTK::CommonCore);geopro_render 已 PUBLIC 传递其余 VTK 组件。 find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore) +# 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) target_sources(geopro_tests PRIVATE render/test_contour_bands.cpp) # dd_voxel:buildVoxel(ScalarVolume->vtkImageData->GPU 体绘制) 构建不崩 + dims 正确。 @@ -117,6 +121,8 @@ target_sources(geopro_tests PRIVATE app/test_scatter_hover.cpp) find_package(Qt6 COMPONENTS Test REQUIRED) target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cpp) target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp) +# VtkSceneController 编排:注入 fake repo + fake view,断言 视图模式×图层 组合下 add 的图元类型/数量;取消勾选清空。 +target_sources(geopro_tests PRIVATE controller/test_vtk_scene_controller.cpp) target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test) add_subdirectory(spike) # spike S3: banded contour 渲染验证 diff --git a/tests/controller/test_vtk_scene_controller.cpp b/tests/controller/test_vtk_scene_controller.cpp new file mode 100644 index 0000000..db8bf2c --- /dev/null +++ b/tests/controller/test_vtk_scene_controller.cpp @@ -0,0 +1,167 @@ +#include + +#include +#include +#include + +#include "I3dSceneView.hpp" +#include "VtkSceneController.hpp" +#include "model/ColorScale.hpp" +#include "model/Field.hpp" +#include "repo/I3dSceneRepository.hpp" +#include "repo/IDatasetRepository.hpp" + +using namespace geopro; +using namespace geopro::controller; + +namespace { + +// 记录视图收到的图元调用类型/数量。 +struct FakeView : I3dSceneView { + int clears = 0; + int surveyLines = 0; + int curtains = 0; + int volumes = 0; + int terrains = 0; + int renders = 0; + bool lastIs2D = false; + double ve = -1.0; + + // clear 模型化"移除所有图元":图元计数归零(反映当前场景状态),clears 累加。 + void clear() override { + ++clears; + surveyLines = curtains = volumes = terrains = 0; + } + void setVerticalExaggeration(double v) override { ve = v; } + void addSurveyLine(const core::Grid&) override { ++surveyLines; } + 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 render(bool is2D) override { ++renders; lastIs2D = is2D; } + + int props() const { return surveyLines + curtains + volumes + terrains; } +}; + +// 同步小数据仓储:loadGrid 返回 2x2 grid,loadColorScale 返回两段色阶。 +struct FakeDsRepo : data::IDatasetRepository { + std::vector loadStructure() override { return {}; } + core::Grid loadGrid(const std::string&) override { + core::Grid g(2, 2); + g.lat = {22.0, 22.001}; + g.lon = {114.0, 114.001}; + return g; + } + core::ScatterField loadScatter(const std::string&) override { return {}; } + core::ColorScale loadColorScale(const std::string&) override { + core::ColorScale cs; + cs.addStop(0.0, core::Rgba{0, 0, 255, 255}); + cs.addStop(1.0, core::Rgba{255, 0, 0, 255}); + return cs; + } + core::ColorScale loadScatterColorScale(const std::string&) override { return loadColorScale(""); } + std::vector loadAnomalies(const std::string&) override { return {}; } +}; + +// 同步三维仓储:dimensionOf 全当 3D;loadVolume 立即回调一个最小有效体。 +struct FakeSceneRepo : data::I3dSceneRepository { + data::DsDimension dimensionOf(const data::DsRow&) const override { + return data::DsDimension::Dim3D; + } + void loadVolume(const std::string&, std::function onOk, + OnError) override { + data::VolumeGrid g; + g.vol = core::ScalarVolume(2, 2, 2); + g.spacing = {{1.0, 1.0, 1.0}}; + g.vmin = 0.0; g.vmax = 1.0; + onOk(std::move(g)); // 同步回调(异步壳) + } + void loadTerrainPaths(std::function onOk, OnError) override { + onOk(data::TerrainPaths{"dem.tif", "image.tif"}); + } +}; + +} // namespace + +// 2D 模式 + 勾选 1 ds → 1 个测线 actor,无帘面/体素/地形。 +TEST(VtkSceneController, Map2DWithOneDatasetAddsSurveyLine) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::Map2D); + c.setCheckedDatasets({"ds1"}); + + EXPECT_EQ(view.surveyLines, 1); + EXPECT_EQ(view.curtains, 0); + EXPECT_EQ(view.volumes, 0); + EXPECT_GE(view.renders, 1); + EXPECT_TRUE(view.lastIs2D); +} + +// 3D 模式 + 帘面图层 → 1 帘面 actor。 +TEST(VtkSceneController, View3DCurtainAddsCurtain) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setCheckedDatasets({"ds1"}); + + EXPECT_EQ(view.curtains, 1); + EXPECT_EQ(view.surveyLines, 0); + EXPECT_FALSE(view.lastIs2D); +} + +// 3D + 帘面 + 体素 → 帘面 1 + 体素 1(体素经异步回调进场)。 +TEST(VtkSceneController, View3DWithVoxelAddsVolume) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setLayer(SceneLayer::Voxel, true); + c.setCheckedDatasets({"ds1"}); + + EXPECT_EQ(view.curtains, 1); + EXPECT_EQ(view.volumes, 1); +} + +// 3D + 地形 → 地形 1(与勾选数据集无关,地形是场景图层)。 +TEST(VtkSceneController, View3DWithTerrainAddsTerrain) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setLayer(SceneLayer::Terrain, true); + c.setCheckedDatasets({"ds1"}); + + EXPECT_EQ(view.terrains, 1); + EXPECT_EQ(view.curtains, 1); +} + +// 取消勾选 → clear 后无任何图元。 +TEST(VtkSceneController, UncheckAllClearsScene) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setCheckedDatasets({"ds1"}); + ASSERT_EQ(view.curtains, 1); + + c.setCheckedDatasets({}); // 取消全部勾选 + EXPECT_EQ(view.curtains, 0); + EXPECT_EQ(view.volumes, 0); + // 最后一次重建仍调用 clear。 + EXPECT_GE(view.clears, 2); +} + +// 纵向比例传到视图。 +TEST(VtkSceneController, VerticalExaggerationForwarded) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setVerticalExaggeration(3.5); + c.setCheckedDatasets({"ds1"}); + EXPECT_DOUBLE_EQ(view.ve, 3.5); +} + +// 多个数据集 → 每个一个帘面。 +TEST(VtkSceneController, MultipleDatasetsAddMultipleCurtains) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setCheckedDatasets({"ds1", "ds2", "ds3"}); + EXPECT_EQ(view.curtains, 3); +} diff --git a/tests/data/test_3d_repo.cpp b/tests/data/test_3d_repo.cpp new file mode 100644 index 0000000..51da60a --- /dev/null +++ b/tests/data/test_3d_repo.cpp @@ -0,0 +1,70 @@ +#include + +#include + +#include "repo/I3dSceneRepository.hpp" +#include "repo/LocalSample3dRepository.hpp" +#include "repo/LocalSampleRepository.hpp" + +using namespace geopro::data; + +static const std::string kDir = + "D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/"; +static const std::string kCrs = "EPSG:4547"; + +namespace { +DsRow rowWith(const std::string& ddCode) { + DsRow r; + r.ddCode = ddCode; + return r; +} +} // namespace + +// dimensionOf:各 ddCode → 维度映射(同步纯函数,spec §6.1)。 +TEST(LocalSample3dRepo, DimensionOfMapsDdCode) { + LocalSampleRepository base(kDir); + LocalSample3dRepository repo(base, kCrs, 22.0, 114.0); + + EXPECT_EQ(repo.dimensionOf(rowWith("dd_voxel")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_Structual3D")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_Property3D")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_section")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_inversion_data")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_slice")), DsDimension::Analysis3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_trajectory_data")), DsDimension::Dim2D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_unknown_xyz")), DsDimension::Other); +} + +// loadVolume:回调收到有效 VolumeGrid(nx>0 且 vmax>vmin),需 PROJ_DATA。 +TEST(LocalSample3dRepo, LoadVolumeCallsBackWithValidGrid) { + LocalSampleRepository base(kDir); + LocalSample3dRepository repo(base, kCrs, 22.0, 114.0); + + bool ok = false; + std::string err; + VolumeGrid got; + repo.loadVolume("voxel1", [&](VolumeGrid g) { ok = true; got = std::move(g); }, + [&](const std::string& m) { err = m; }); + + ASSERT_TRUE(ok) << "loadVolume onErr: " << err; + EXPECT_GT(got.vol.nx(), 0); + EXPECT_GT(got.vol.ny(), 0); + EXPECT_GT(got.vol.nz(), 0); + EXPECT_GT(got.vmax, got.vmin); + EXPECT_TRUE(got.valid()); +} + +// loadTerrainPaths:回调收到 dem/image 绝对路径(非空)。 +TEST(LocalSample3dRepo, LoadTerrainPathsCallsBack) { + LocalSampleRepository base(kDir); + LocalSample3dRepository repo(base, kCrs, 22.0, 114.0); + + bool ok = false; + TerrainPaths got; + repo.loadTerrainPaths([&](TerrainPaths p) { ok = true; got = std::move(p); }, + [&](const std::string&) {}); + + ASSERT_TRUE(ok); + EXPECT_FALSE(got.demPath.empty()); + EXPECT_FALSE(got.imagePath.empty()); +} diff --git a/tests/render/test_scene.cpp b/tests/render/test_scene.cpp new file mode 100644 index 0000000..ec3fad1 --- /dev/null +++ b/tests/render/test_scene.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include + +#include "Scene.hpp" + +using geopro::render::Scene; + +// addActor 把 vtkActor 加入 renderer,view prop 计数 +1。 +TEST(SceneTest, AddActorIncrementsViewProps) { + Scene scene; + EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 0); + auto a = vtkSmartPointer::New(); + scene.addActor(a); + EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 1); +} + +// addViewProp 接受 vtkVolume(vtkProp3D,非 vtkActor)——体绘制必经此口。 +TEST(SceneTest, AddViewPropAcceptsVolume) { + Scene scene; + auto vol = vtkSmartPointer::New(); + scene.addViewProp(vol); + EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 1); +} + +// clear() 经 RemoveAllViewProps 清空 actor 与 volume(覆盖体绘制 prop)。 +TEST(SceneTest, ClearRemovesActorsAndVolumes) { + Scene scene; + scene.addActor(vtkSmartPointer::New()); + scene.addViewProp(vtkSmartPointer::New()); + EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 2); + scene.clear(); + EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 0); +} + +// 空指针安全:addActor/addViewProp(nullptr) 不崩、不增计数。 +TEST(SceneTest, NullPropsAreIgnored) { + Scene scene; + scene.addActor(nullptr); + scene.addViewProp(nullptr); + EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 0); +} diff --git a/vcpkg.json b/vcpkg.json index f515a80..1458de6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,6 +2,7 @@ "name": "geopro-desktop", "version": "0.1.0", "description": "Geopro 3.0 desktop client (Qt6 + VTK9) - M1. 方案②-修订: Qt/VTK/ADS/QtKeychain 对接官方 MSVC Qt(不走 vcpkg); 仅非 Qt 依赖走 vcpkg, 按层递增。", + "builtin-baseline": "10ceb139a610ebf3c6aa49cdc4a4b7f3db5d3f2b", "dependencies": [ "eigen3", "gdal",