332 lines
13 KiB
C++
332 lines
13 KiB
C++
#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 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)
|
||
|
||
// clear 模型化"移除所有数据图元":计数归零,clears 累加。
|
||
void clear() override {
|
||
++clears;
|
||
surveyLines = curtains = volumes = terrains = 0;
|
||
perDs.clear();
|
||
}
|
||
void setVerticalExaggeration(double v) override { ve = v; }
|
||
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&) override {
|
||
++volumes;
|
||
++perDs[dsId].second;
|
||
}
|
||
void addTerrain(const data::TerrainPaths&) override { ++terrains; }
|
||
void removeDataset(const std::string& dsId) override {
|
||
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 {}
|
||
|
||
int props() const { return surveyLines + curtains + volumes + terrains; }
|
||
};
|
||
|
||
// 同步小数据仓储:loadGrid 返回 2x2 grid,loadColorScale 返回两段色阶。
|
||
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 全当 3D;loadVolume 立即回调一个最小有效体。
|
||
struct FakeSceneRepo : data::I3dSceneRepository {
|
||
data::DsDimension dimensionOf(const data::DsRow&) const override {
|
||
return data::DsDimension::Dim3D;
|
||
}
|
||
// 按数据集类型分流(取代旧全局 showVoxel/showCurtain):volumeIds 内 → 体素,否则帘面。
|
||
// 默认空 → 全走帘面(同旧默认行为);体素测试显式标记某 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 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 图元(不整场 clear,3D 增量路径)。
|
||
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);
|
||
}
|
||
|
||
// ── 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);
|
||
}
|