geopro/tests/controller/test_vtk_scene_controller.cpp

503 lines
20 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;
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) override { ++renders; lastIs2D = is2D; }
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
// 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、帘面 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({"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({"ds1"});
EXPECT_EQ(view.terrains, 1);
EXPECT_EQ(view.curtains, 1);
}
// 取消勾选 → 增量移除该 ds 图元(不整场 clear3D 增量路径)。
TEST(VtkSceneController, UncheckRemovesDatasetIncrementally) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"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({"ds1"});
const int clearsAfterFirst = view.clears;
ASSERT_EQ(view.curtains, 1);
c.setCheckedDatasets({"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({"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);
}
// ── 色阶编辑器「确定」setVolumeColorScale ──
// 已渲染三维体改色阶 → 移除旧体素 + 以新色阶重建(体素计数不变,但新色阶下发)。
TEST(VtkSceneController, SetVolumeColorScaleRebuildsCheckedVolume) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
sc.volumeIds = {"ds1"};
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"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({"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({"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);
}
// ── 二维数据集视图:足迹平铺进 View3D ──
// 默认摆放模式=关闭(0) → 勾选 2D 足迹不渲染(仅记录勾选)。
TEST(VtkSceneController, TwoDPlacementOffDoesNotRender) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setChecked2DDatasets({"traj1"}); // 摆放默认关闭
EXPECT_EQ(view.mapLines, 0);
}
// 摆放 Z=0(1) + 勾选足迹 → 1 条 mapLineworldZ=0不影响帘面/体素计数。
TEST(VtkSceneController, TwoDPlacementZeroAddsMapLine) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.set2DPlacement(1, 0.0); // Z=0
c.setChecked2DDatasets({"traj1"});
EXPECT_EQ(view.mapLines, 1);
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0);
EXPECT_EQ(view.curtains, 0);
EXPECT_EQ(view.volumes, 0);
}
// 顶部/底部摆放锚定真实地表高程worldZ = zRefElev ± 偏移(而非世界 0 ± 偏移)。
TEST(VtkSceneController, TwoDPlacementTopBottomAnchorToSurfaceElev) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
view.refElev = 1200.0; // 地表高程基准
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.set2DPlacement(2, 0.0); // 顶部
c.setChecked2DDatasets({"traj1"});
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 1200.0 + 50.0); // 贴地表上方
c.set2DPlacement(3, 0.0); // 底部
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 1200.0 - 50.0); // 地表下方
}
// 取消勾选 2D 足迹 → 增量移除该足迹图元(不整场 clear
TEST(VtkSceneController, TwoDUncheckRemovesMapLine) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.set2DPlacement(1, 0.0);
c.setChecked2DDatasets({"traj1"});
ASSERT_EQ(view.mapLines, 1);
const int clearsBefore = view.clears;
c.setChecked2DDatasets({}); // 取消勾选
EXPECT_EQ(view.mapLines, 0);
EXPECT_EQ(view.clears, clearsBefore); // 增量移除,不整场 clear
}
// 2D 足迹与 3D 帘面共存且独立:勾选剖面 + 足迹,各出各的图元,互不影响。
TEST(VtkSceneController, TwoDFootprintCoexistsWith3DCurtain) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"prof1"}); // 3D 帘面
c.set2DPlacement(1, 0.0);
c.setChecked2DDatasets({"traj1"}); // 2D 足迹
EXPECT_EQ(view.curtains, 1);
EXPECT_EQ(view.mapLines, 1);
c.setChecked2DDatasets({}); // 取消足迹 → 帘面不受影响
EXPECT_EQ(view.mapLines, 0);
EXPECT_EQ(view.curtains, 1);
}
// 自定义摆放(4) → worldZ=customZ改摆放重摆已勾选足迹移除旧 + 按新 Z 重加)。
TEST(VtkSceneController, TwoDPlacementCustomZAndReplace) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.set2DPlacement(4, 123.5); // 自定义 Z
c.setChecked2DDatasets({"traj1"});
ASSERT_EQ(view.mapLines, 1);
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 123.5);
const int removesBefore = view.removeCalls;
c.set2DPlacement(4, 200.0); // 改自定义 Z → 重摆
EXPECT_EQ(view.mapLines, 1); // 移除 1 + 新增 1 → 净计数不变
EXPECT_EQ(view.removeCalls, removesBefore + 1); // 旧足迹被移除
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 200.0); // 新 Z 已下发
}
// 摆放从关闭(0)切到 Z=0(1) → 已勾选但未渲染的足迹补画。
TEST(VtkSceneController, TwoDPlacementOffToOnDrawsCheckedFootprint) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setChecked2DDatasets({"traj1"}); // 关闭态:仅记录
ASSERT_EQ(view.mapLines, 0);
c.set2DPlacement(1, 0.0); // 切到 Z=0 → 补画
EXPECT_EQ(view.mapLines, 1);
}