geopro/tests/controller/test_vtk_scene_controller.cpp

466 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <gtest/gtest.h>
#include <functional>
#include <map>
#include <set>
#include <string>
#include <utility>
#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 mapLines = 0;
int terrains = 0;
int renders = 0;
bool lastIs2D = false;
bool lastResetCamera = true;
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;
// 按 ds 记录图元数,使 removeDataset 能按量回退(反映增量场景状态)。
std::map<std::string, std::pair<int, int>> perDs; // dsId → (curtains, volumes)
std::map<std::string, int> perDsMapLines; // dsId → mapLine 数removeDataset 回退用)
double lastMapLineZ = 0.0; // 最近一次 addMapLine 的 worldZ摆放验证
double refElev = 0.0; // 地表高程基准(顶/底摆放锚定)
// 色阶编辑:记录最近一次 addVolume 收到的色阶 + removeDataset 调用数(验证就地重渲染)。
core::ColorScale lastVolumeScale;
int removeCalls = 0;
// clear 模型化"移除所有数据图元"计数归零clears 累加。
void clear() override {
++clears;
surveyLines = curtains = volumes = mapLines = terrains = 0;
perDs.clear();
perDsMapLines.clear();
}
void setVerticalExaggeration(double v) override { ve = v; }
double zRefElev() const override { return refElev; }
void addSurveyLine(const core::Grid&) override { ++surveyLines; }
void addCurtain(const std::string& dsId, const core::Grid&, const core::ColorScale&) override {
++curtains;
++perDs[dsId].first;
}
void addVolume(const std::string& dsId, const data::VolumeGrid&,
const core::ColorScale& cs) override {
++volumes;
++perDs[dsId].second;
lastVolumeScale = cs;
}
void addMapLine(const std::string& dsId, const data::MapLine&, double worldZ) override {
++mapLines;
++perDsMapLines[dsId];
lastMapLineZ = worldZ;
}
void addTerrain(const data::TerrainPaths&) override { ++terrains; }
void removeDataset(const std::string& dsId) override {
++removeCalls;
auto ml = perDsMapLines.find(dsId);
if (ml != perDsMapLines.end()) {
mapLines -= ml->second;
perDsMapLines.erase(ml);
}
auto it = perDs.find(dsId);
if (it == perDs.end()) return;
curtains -= it->second.first;
volumes -= it->second.second;
perDs.erase(it);
}
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, bool resetCamera) override {
++renders;
lastIs2D = is2D;
lastResetCamera = resetCamera;
}
void renderIncremental() override { ++renders; }
// 异常(#4测试不断言异常渲染空实现满足接口。
void addAnomaly(const core::Anomaly&) override {}
void removeAnomaly(const std::string&) override {}
void clearAnomalies() override {}
void setAnomalyVisible(const std::string&, bool) override {}
void setSelectedAnomaly(const std::string&) override {}
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;
}
// 按数据集类型分流(取代旧全局 showVoxel/showCurtainvolumeIds 内 → 体素,否则帘面。
// 默认空 → 全走帘面(同旧默认行为);体素测试显式标记某 ds 为体素类型。
std::set<std::string> volumeIds;
bool isVolumeDataset(const std::string& dsId) const override {
return volumeIds.count(dsId) > 0;
}
void loadVolume(const std::string&,
std::function<void(data::VolumeGrid, core::ColorScale)> 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;
core::ColorScale cs;
cs.addStop(0.0, core::Rgba{0, 0, 255, 255});
cs.addStop(1.0, core::Rgba{255, 0, 0, 255});
onOk(std::move(g), cs); // 同步回调(异步壳)
}
void loadSection(const std::string&, std::function<void(data::SectionData)> onOk,
OnError) override {
data::SectionData s;
s.grid = core::Grid(2, 2);
s.grid.lat = {22.0, 22.001};
s.grid.lon = {114.0, 114.001};
s.scale.addStop(0.0, core::Rgba{0, 0, 255, 255});
s.scale.addStop(1.0, core::Rgba{255, 0, 0, 255});
onOk(std::move(s)); // 同步回调(异步壳)
}
void loadMapLine(const std::string&, std::function<void(data::MapLine)> onOk,
OnError) override {
data::MapLine line;
line.lat = {22.0, 22.001, 22.002};
line.lon = {114.0, 114.001, 114.002};
onOk(std::move(line)); // 同步回调(异步壳)
}
void loadTerrainPaths(std::function<void(data::TerrainPaths)> onOk, OnError) override {
onOk(data::TerrainPaths{"dem.tif", "image.tif"});
}
// 切片/异常/任务 stub满足纯虚行为同 LocalSample3dRepository
void createSlice(const SliceSpec&, const std::string&,
std::function<void(std::string)> onOk, OnError) override { onOk("slice-0"); }
void saveSlice(const std::string&, const SliceSpec&,
std::function<void()> onOk, OnError) override { onOk(); }
void deleteSlice(const std::string&,
std::function<void()> onOk, OnError) override { onOk(); }
void loadAnomalyTree(const std::string&,
std::function<void(AnomalyTree)> onOk, OnError) override { onOk({}); }
void saveAnomaly(const core::Anomaly&, const std::string&,
std::function<void(std::string)> onOk, OnError) override { onOk("anomaly-0"); }
void deleteAnomaly(const std::string&,
std::function<void()> onOk, OnError) override { onOk(); }
void deleteAnomalyGroup(const std::string&,
std::function<void()> onOk, OnError) override { onOk(); }
void loadTaskRecords(const std::string&,
std::function<void(std::vector<TaskRecord>)> onOk, OnError) override { onOk({}); }
void loadUsableTasks(const std::string&,
std::function<void(std::vector<UsableTask>)> onOk, OnError) override { onOk({}); }
};
} // namespace
// B2 后勾选统一入口 = (dsId, typeId=描述符 id) 列表。便捷构造:电阻率(curtain)/三维体(volume)/轨迹(plane2d)。
namespace {
using IdType = std::vector<std::pair<std::string, std::string>>;
IdType curtainIds(std::initializer_list<std::string> ids) {
IdType v;
for (const auto& id : ids) v.push_back({id, "resistivity"}); // resistivity → renderStrategyId "curtain"
return v;
}
IdType voxelIds(std::initializer_list<std::string> ids) {
IdType v;
for (const auto& id : ids) v.push_back({id, "voxel"}); // voxel → "volume"
return v;
}
} // namespace
// 3D 帘面:勾选电阻率(curtain 策略) → 1 帘面 actor。
TEST(VtkSceneController, View3DCurtainAddsCurtain) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(curtainIds({"ds1"}));
EXPECT_EQ(view.curtains, 1);
EXPECT_EQ(view.surveyLines, 0);
EXPECT_FALSE(view.lastIs2D);
}
// 3D + 体素类型数据集 → 体素 1、帘面 0按类型分流体素 XOR 帘面,一个 ds 只一种表示)。
TEST(VtkSceneController, View3DVolumeDatasetAddsVolume) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
sc.volumeIds = {"ds1"}; // ds1 = 三维体类型 → 体素渲染路径
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(voxelIds({"ds1"}));
EXPECT_EQ(view.volumes, 1);
EXPECT_EQ(view.curtains, 0); // 体素数据集不再同时出帘面
}
// 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(curtainIds({"ds1"}));
EXPECT_EQ(view.terrains, 1);
EXPECT_EQ(view.curtains, 1);
}
// 取消勾选 → 增量移除该 ds 图元(不整场 clear增量路径
TEST(VtkSceneController, UncheckRemovesDatasetIncrementally) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(curtainIds({"ds1"}));
ASSERT_EQ(view.curtains, 1);
const int clearsAfterCheck = view.clears;
c.setCheckedDatasets({}); // 取消全部勾选 → 增量移除 ds1
EXPECT_EQ(view.curtains, 0);
EXPECT_EQ(view.volumes, 0);
EXPECT_EQ(view.clears, clearsAfterCheck); // 增量取消不触发整场 clear
}
// 增量追加:已勾选 ds1 时再勾 ds2只新增 ds2不移除/重建 ds1。
TEST(VtkSceneController, IncrementalAddKeepsExisting) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(curtainIds({"ds1"}));
const int clearsAfterFirst = view.clears;
ASSERT_EQ(view.curtains, 1);
c.setCheckedDatasets(curtainIds({"ds1", "ds2"})); // 增量加 ds2
EXPECT_EQ(view.curtains, 2);
EXPECT_EQ(view.clears, clearsAfterFirst); // 不重建 → 无新 clear
}
// 纵向比例传到视图。
TEST(VtkSceneController, VerticalExaggerationForwarded) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setVerticalExaggeration(3.5);
c.setCheckedDatasets(curtainIds({"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(curtainIds({"ds1", "ds2", "ds3"}));
EXPECT_EQ(view.curtains, 3);
}
// ── 色阶编辑器「确定」setVolumeColorScale ──
// 已渲染三维体改色阶 → 移除旧体素 + 以新色阶重建(体素计数不变,但新色阶下发)。
TEST(VtkSceneController, SetVolumeColorScaleRebuildsCheckedVolume) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
sc.volumeIds = {"ds1"};
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(voxelIds({"ds1"}));
ASSERT_EQ(view.volumes, 1);
const int removesBefore = view.removeCalls;
core::ColorScale edited; // 三段(与初始两段区分)
edited.addStop(0.0, core::Rgba{0, 0, 0, 255});
edited.addStop(0.5, core::Rgba{128, 128, 128, 255});
edited.addStop(1.0, core::Rgba{255, 255, 255, 255});
c.setVolumeColorScale("ds1", edited);
EXPECT_EQ(view.volumes, 1); // 移除 1 + 新增 1 → 净计数不变
EXPECT_EQ(view.removeCalls, removesBefore + 1); // 旧体素被移除
EXPECT_EQ(view.lastVolumeScale.stopCount(), 3u); // 新色阶(三段)已下发
}
// 会话级 mock 持久:已加载的体编辑色阶后,取消再勾选仍用编辑后的色阶(命中缓存,不回退默认)。
TEST(VtkSceneController, SetVolumeColorScalePersistsAcrossRecheck) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
sc.volumeIds = {"ds1"};
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(voxelIds({"ds1"})); // 加载体(填充 volumeCache_
ASSERT_EQ(view.lastVolumeScale.stopCount(), 2u); // 初始两段
core::ColorScale edited; // 编辑成三段
edited.addStop(0.0, core::Rgba{0, 0, 0, 255});
edited.addStop(0.5, core::Rgba{128, 128, 128, 255});
edited.addStop(1.0, core::Rgba{255, 255, 255, 255});
c.setVolumeColorScale("ds1", edited);
ASSERT_EQ(view.lastVolumeScale.stopCount(), 3u);
c.setCheckedDatasets({}); // 取消勾选
c.setCheckedDatasets(voxelIds({"ds1"})); // 再勾选 → 命中缓存(含编辑后色阶)
EXPECT_EQ(view.lastVolumeScale.stopCount(), 3u);
}
// ── 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);
}
// ── 二维数据集(轨迹/足迹)经 plane2d 策略平铺进场景 ──
// B2去 col2D + setChecked2DDatasets/set2DPlacement 公有入口2D 与 3D 合一经统一入口
// setCheckedDatasets((dsId, typeId))。trajectory 描述符 → "plane2d" 策略 → add2DDatasetAsync。
// 摆放暂固定默认(Z=0);置/底/自定义 + analysisMode 取景基线相关用例随旧入口移除Phase E/F 重接后补)。
namespace {
IdType trajIds(std::initializer_list<std::string> ids) {
IdType v;
for (const auto& id : ids) v.push_back({id, "trajectory"}); // trajectory → renderStrategyId "plane2d"
return v;
}
} // namespace
// 勾选轨迹(plane2d 策略) → 1 条 mapLine默认摆放 worldZ=0不影响帘面/体素计数。
TEST(VtkSceneController, TrajectoryRendersAsMapLineAtDefaultZero) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(trajIds({"traj1"}));
EXPECT_EQ(view.mapLines, 1);
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0);
EXPECT_EQ(view.curtains, 0);
EXPECT_EQ(view.volumes, 0);
}
// 取消勾选轨迹 → 增量移除该足迹图元(不整场 clear
TEST(VtkSceneController, TrajectoryUncheckRemovesMapLine) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets(trajIds({"traj1"}));
ASSERT_EQ(view.mapLines, 1);
const int clearsBefore = view.clears;
c.setCheckedDatasets({}); // 取消勾选
EXPECT_EQ(view.mapLines, 0);
EXPECT_EQ(view.clears, clearsBefore); // 增量移除,不整场 clear
}
// 轨迹足迹与 3D 帘面经同一入口共存且独立:各出各的图元,取消足迹不影响帘面。
TEST(VtkSceneController, TrajectoryCoexistsWith3DCurtain) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
IdType both = curtainIds({"prof1"});
both.push_back({"traj1", "trajectory"});
c.setCheckedDatasets(both); // 帘面 + 足迹(统一入口并集)
EXPECT_EQ(view.curtains, 1);
EXPECT_EQ(view.mapLines, 1);
c.setCheckedDatasets(curtainIds({"prof1"})); // 仅留帘面 → 足迹移除
EXPECT_EQ(view.mapLines, 0);
EXPECT_EQ(view.curtains, 1);
}