feat(vtk): P1 复活中央渲染 — VtkSceneController + I3dSceneRepository + Scene加vtkProp

复活被 6241eb3 摘除的中央 VTK 数据驱动渲染:
- Scene 新增 addViewProp(vtkProp*):体绘制 vtkVolume(非 vtkActor)经此进场
- I3dSceneRepository(异步回调契约) + LocalSample3dRepository:dimensionOf 维度映射 /
  loadVolume→VolumeGrid(std::array 去裸数组) / loadTerrainPaths;data 层零 VTK 依赖
- VtkSceneController(QObject) 取代 main.cpp 死掉的 rebuildCentral lambda + 裸 show* 标志:
  勾选数据集/视图模式/图层/比例 → 经仓储取 core::* → I3dSceneView 重建场景;
  QPointer+generation 守异步回调生命周期与新鲜度;inRebuild_ 避免同步路径双 render
- I3dSceneView 抽象解耦编排与 VTK(VtkSceneView 真实现 + 测试 fake)
- 删除被取代的 CentralScene;main.cpp 接线 对象勾选/2D-3D/图层/主题(主题 context 用 sceneCtrl 防悬垂)
- 新增测试 14(Scene/3d-repo/VtkSceneController),ctest 172/172 全绿

构建基建修复(本就潜在缺陷,任何 clean 构建/新人 checkout 都会撞):
- vcpkg.json 加 builtin-baseline:新版 vcpkg manifest 模式必需,否则全新 checkout 无法 configure
- build.bat 修 vswhere(VS2026 预览 -latest 恒空 → -all -prerelease -requires VC.Tools)
  + 括号块内路径变量加引号(防 Program Files (x86) 的 ) 提前闭合)
This commit is contained in:
gaozheng 2026-06-15 21:01:26 +08:00
parent 918088e67a
commit 0f521c5b24
22 changed files with 953 additions and 152 deletions

View File

@ -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

View File

@ -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

View File

@ -1,49 +0,0 @@
#include "CentralScene.hpp"
#include <vtkActor.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#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<SectionInput>& 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

View File

@ -1,31 +0,0 @@
#pragma once
#include <vector>
#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 };
// 一个待渲染剖面grid2D 测线 / 3D 帘面都用)+ colorScale3D 帘面上色)。
struct SectionInput {
geopro::core::Grid grid;
geopro::core::ColorScale colorScale;
};
// 中央场景重建(脱离对象树,按显式 sections 渲染):
// 2D = 每个 section 的 buildSurveyLine3D = 每个 section 的 buildCurtain受 showCurtain
// 下一轮接真实 DS构建 sections 后调用本函数即可render 层零改动。
void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
vtkRenderWindow* renderWindow, ViewMode mode,
const std::vector<SectionInput>& sections, bool showCurtain,
const geopro::core::GeoLocalFrame& frame, double verticalExaggeration);
} // namespace geopro::app

72
src/app/VtkSceneView.cpp Normal file
View File

@ -0,0 +1,72 @@
#include "VtkSceneView.hpp"
#include <utility>
#include <vtkActor.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkVolume.h>
#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<geopro::core::GeoLocalFrame> 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

39
src/app/VtkSceneView.hpp Normal file
View File

@ -0,0 +1,39 @@
#pragma once
#include <memory>
#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<geopro::core::GeoLocalFrame> 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<geopro::core::GeoLocalFrame> frame_;
double zRefElev_;
double verticalExaggeration_ = 2.0;
};
} // namespace geopro::app

View File

@ -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 <vtkCamera.h>
#include <vtkCameraInterpolator.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h>
#include <vtkProperty.h>
#include <vtkRenderWindowInteractor.h>
@ -199,9 +200,6 @@ double median(std::vector<double> 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<vtkGenericOpenGLRenderWindow> renderWindow;
vtkWidget->setRenderWindow(renderWindow);
renderWindow->AddRenderer(scene->renderer());
vtkRenderer* rendererPtr = scene->renderer();
vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get();
// 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。
auto viewMode = std::make_shared<ViewMode>(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<bool>(true); // 帘面,默认显示
auto showVoxel = std::make_shared<bool>(false); // 体素,默认关
auto showTerrain = std::make_shared<bool>(false); // 地形(DEM+影像),默认关
auto showSlice = std::make_shared<bool>(false); // dd_slice 交互切片,默认关
// 持久的切片 widget(挂 interactor跨重建保活rebuildCentral 据条件创建/拆除)。
auto slicePlane = std::make_shared<vtkSmartPointer<vtkImagePlaneWidget>>();
std::shared_ptr<geopro::core::CrsTransform> crs; // PROJ 失败→空→体素层无效(不崩)
// PROJ 可用性(体素/地形/切片层都需配准):失败则浮层相应勾选禁用并提示。
bool crsAvailable = false;
try {
crs = std::make_shared<geopro::core::CrsTransform>(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<geopro::app::SectionInput>{}, *showCurtain,
*frame, kVerticalExaggeration);
};
// 中央渲染由 sceneCtrlVtkSceneController驱动勾选对象/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非 windowThemeManager 是进程级单例,连接须随 sceneCtrl 析构自动断开,
// 否则 window 析构期间 sceneCtrl(其孙级子对象)已销毁、主题异步变化会触悬垂指针。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, sceneCtrl,
[sceneCtrl]() { sceneCtrl->rebuild(); });
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。

View File

@ -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)

View File

@ -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;
// 3DDEM 地形 + 影像纹理。
virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0;
// 应用相机预设2D 俯视 / 3D 自由)并提交渲染。
virtual void render(bool is2D) = 0;
};
} // namespace geopro::controller

View File

@ -0,0 +1,121 @@
#include "VtkSceneController.hpp"
#include <utility>
#include <QPointer>
#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<std::size_t>(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;
// 坏 dsIdloadGrid/loadColorScale 抛异常)= best-effort 跳过emit loadFailed 但不中断,
// 其余勾选数据集照常渲染(非 fail-fast
try {
if (is2D) {
for (const auto& dsId : checkedDs_) view_.addSurveyLine(grid(dsId));
} else {
// 回调用 QPointer<self> 守对象存活(控制器是 QObject+ gen 守数据新鲜:
// 将来 Api 实现在网络线程迟到回调时self 已析构则直接丢弃,不触 dangling。
QPointer<VtkSceneController> 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

View File

@ -0,0 +1,78 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <map>
#include <optional>
#include <string>
#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<std::string> checkedDs_;
ViewMode mode_ = ViewMode::Map2D;
bool showCurtain_ = true;
bool showVoxel_ = false;
bool showTerrain_ = false;
double verticalExaggeration_ = 2.0;
// 缓存(按 dsId避免重复读盘/插值。
std::map<std::string, geopro::core::Grid> gridCache_;
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
std::map<std::string, data::VolumeGrid> 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

View File

@ -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

View File

@ -0,0 +1,52 @@
#pragma once
#include <array>
#include <functional>
#include <string>
#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::arrayspec §6.2)。
struct VolumeGrid {
geopro::core::ScalarVolume vol{0, 0, 0};
std::array<double, 3> origin{{0.0, 0.0, 0.0}}; // ox, oy, oz世界米
std::array<double, 3> 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::functionLocalSample 本地数据同步算好后直接回调;
// 将来 Api3dRepository 在网络完成时回调,上层不变)。
// **契约onOk/onErr 必须在主(GUI)线程调用**——上层(VtkSceneController)回调内直接操作
// 场景/发 Qt 信号依赖主线程亲和Api 实现若在工作线程完成须 post 回主线程再回调。
// dimensionOf 是同步纯函数(无 I/O只做类型→维度映射
// 切片/异常/任务等签名本期不在接口内(留 P3/P4
class I3dSceneRepository {
public:
using OnError = std::function<void(const std::string& message)>;
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<void(VolumeGrid)> onOk, OnError onErr) = 0;
// 异步:加载地形 DEM/影像路径。
virtual void loadTerrainPaths(std::function<void(TerrainPaths)> onOk, OnError onErr) = 0;
};
} // namespace geopro::data

View File

@ -0,0 +1,159 @@
#include "repo/LocalSample3dRepository.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <exception>
#include <limits>
#include <utility>
#include <vector>
#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<int>(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<void(VolumeGrid)> onOk, OnError onErr) {
// P1 样本dsId 暂未使用,固定读同一组交叉剖面散点→体素(真实 Api 实现按 dsId 取)。
try {
// 1) 读两条交叉剖面散点 + 色阶;配准到世界局部米 + 深度,组装 IDW 输入点集。
const std::vector<ScatterField> 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 → ScalarVolumemaxDist 外 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<double> stops = cs.stopValues();
if (stops.size() >= 2) {
vmin = stops.front(); vmax = stops.back();
} else {
vmin = std::numeric_limits<double>::infinity();
vmax = -std::numeric_limits<double>::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<void(TerrainPaths)> 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

View File

@ -0,0 +1,34 @@
#pragma once
#include <functional>
#include <string>
#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<void(VolumeGrid)> onOk,
OnError onErr) override;
void loadTerrainPaths(std::function<void(TerrainPaths)> onOk, OnError onErr) override;
private:
LocalSampleRepository& base_;
std::string projectCrs_;
double baseLat_, baseLon_;
};
} // namespace geopro::data

View File

@ -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

View File

@ -2,6 +2,7 @@
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkActor.h>
#include <vtkProp.h>
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<vtkRenderer> renderer_;

View File

@ -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/LocalSample3dRepositorydimensionOf + 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 ColorLutBuildercore ColorScale -> vtkLookupTable
# vtkLookupTableVTK::CommonCoregeopro_render PUBLIC VTK
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore)
# SceneaddActor/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_voxelbuildVoxel(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

View File

@ -0,0 +1,167 @@
#include <gtest/gtest.h>
#include <functional>
#include <string>
#include <vector>
#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 gridloadColorScale 返回两段色阶。
struct FakeDsRepo : data::IDatasetRepository {
std::vector<data::GsNode> 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<core::Anomaly> loadAnomalies(const std::string&) override { return {}; }
};
// 同步三维仓储dimensionOf 全当 3DloadVolume 立即回调一个最小有效体。
struct FakeSceneRepo : data::I3dSceneRepository {
data::DsDimension dimensionOf(const data::DsRow&) const override {
return data::DsDimension::Dim3D;
}
void loadVolume(const std::string&, std::function<void(data::VolumeGrid)> 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<void(data::TerrainPaths)> 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);
}

View File

@ -0,0 +1,70 @@
#include <gtest/gtest.h>
#include <string>
#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回调收到有效 VolumeGridnx>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());
}

View File

@ -0,0 +1,45 @@
#include <gtest/gtest.h>
#include <vtkActor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
#include <vtkVolume.h>
#include "Scene.hpp"
using geopro::render::Scene;
// addActor 把 vtkActor 加入 rendererview prop 计数 +1。
TEST(SceneTest, AddActorIncrementsViewProps) {
Scene scene;
EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 0);
auto a = vtkSmartPointer<vtkActor>::New();
scene.addActor(a);
EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 1);
}
// addViewProp 接受 vtkVolumevtkProp3D非 vtkActor——体绘制必经此口。
TEST(SceneTest, AddViewPropAcceptsVolume) {
Scene scene;
auto vol = vtkSmartPointer<vtkVolume>::New();
scene.addViewProp(vol);
EXPECT_EQ(scene.renderer()->GetViewProps()->GetNumberOfItems(), 1);
}
// clear() 经 RemoveAllViewProps 清空 actor 与 volume覆盖体绘制 prop
TEST(SceneTest, ClearRemovesActorsAndVolumes) {
Scene scene;
scene.addActor(vtkSmartPointer<vtkActor>::New());
scene.addViewProp(vtkSmartPointer<vtkVolume>::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);
}

View File

@ -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",