#include #include #include #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 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> perDs; // dsId → (curtains, volumes) std::map 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 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; } // 按数据集类型分流(取代旧全局 showVoxel/showCurtain):volumeIds 内 → 体素,否则帘面。 // 默认空 → 全走帘面(同旧默认行为);体素测试显式标记某 ds 为体素类型。 std::set volumeIds; bool isVolumeDataset(const std::string& dsId) const override { return volumeIds.count(dsId) > 0; } 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; 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 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 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 onOk, OnError) override { onOk(data::TerrainPaths{"dem.tif", "image.tif"}); } // 切片/异常/任务 stub(满足纯虚,行为同 LocalSample3dRepository) void createSlice(const SliceSpec&, const std::string&, std::function onOk, OnError) override { onOk("slice-0"); } void saveSlice(const std::string&, const SliceSpec&, std::function onOk, OnError) override { onOk(); } void deleteSlice(const std::string&, std::function onOk, OnError) override { onOk(); } void loadAnomalyTree(const std::string&, std::function onOk, OnError) override { onOk({}); } void saveAnomaly(const core::Anomaly&, const std::string&, std::function onOk, OnError) override { onOk("anomaly-0"); } void deleteAnomaly(const std::string&, std::function onOk, OnError) override { onOk(); } void deleteAnomalyGroup(const std::string&, std::function onOk, OnError) override { onOk(); } void loadTaskRecords(const std::string&, std::function)> onOk, OnError) override { onOk({}); } void loadUsableTasks(const std::string&, std::function)> 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); } // ── 色阶编辑器「确定」: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.set2DPlacement(0, 0.0); // 显式关闭 c.setChecked2DDatasets({"traj1"}); EXPECT_EQ(view.mapLines, 0); } // 回归(足迹默认不渲染 bug):默认摆放=Z=0(1),与 2D视图下拉可见默认项一致 → // 仅勾选 2D 足迹(不手动调 set2DPlacement)即应在 View3D 渲染,worldZ=0。 TEST(VtkSceneController, TwoDDefaultPlacementRendersAtZeroOnCheck) { FakeDsRepo ds; FakeSceneRepo sc; FakeView view; VtkSceneController c(ds, sc, view); c.setViewMode(ViewMode::View3D); c.setChecked2DDatasets({"traj1"}); // 不调 set2DPlacement,依赖默认摆放 EXPECT_EQ(view.mapLines, 1); EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0); } // 摆放 Z=0(1) + 勾选足迹 → 1 条 mapLine,worldZ=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.set2DPlacement(0, 0.0); // 显式关闭(默认已是 Z=0) c.setChecked2DDatasets({"traj1"}); // 关闭态:仅记录 ASSERT_EQ(view.mapLines, 0); c.set2DPlacement(1, 0.0); // 切到 Z=0 → 补画 EXPECT_EQ(view.mapLines, 1); }