refactor(vtk): 抽屉去tab单列; 勾选经描述符路由渲染策略(统一入口,无维度散判)

- ColumnDrawer 去 QTabWidget/Column2DDataset/analysisModeChanged, body 直持 CategoryAnalysisTab 单列
- VtkSceneController: 统一入口 setCheckedDatasets((dsId,typeId)) 经 catalog.renderStrategyId 派 3 策略
  add/remove, 维护每 typeId 活跃计数 onTypeActivated/Deactivated; checkedDs_/checked2dDs_ 合并为 checked_
- 3 策略持控制器友元引用, 委托回 addDatasetAsync/add2DDatasetAsync/view.removeDataset
- main: pushChecked 解析 dsId→typeId 下发统一入口; 删 col2D 全部接线; 2D 轨迹经 trajectory 段并入单列
- rebuildInternal 据 checked_ 经策略重放(主题/VE/坐标轴重建不丢数据)
- 控制器单测迁移至统一 API (18/18 通过)
This commit is contained in:
gaozheng 2026-06-30 21:56:37 +08:00
parent 286054720e
commit 70f847058d
8 changed files with 199 additions and 353 deletions

View File

@ -95,6 +95,7 @@
#include "AuthService.hpp"
#include "DatasetDimension.hpp"
#include "DatasetCategory.hpp"
#include "repo/CategoryDescriptor.hpp"
#include "Credential.hpp"
#include "Glyphs.hpp"
#include "Logging.hpp"
@ -149,7 +150,6 @@
#include "AxesSettingsDialog.hpp"
#include "AxesSettingsPanel.hpp"
#include "repo/DatasetFieldDictionary.hpp"
#include "panels/columns/Column2DDataset.hpp"
#include "CameraPreset.hpp"
#include "ColorLutBuilder.hpp"
@ -556,20 +556,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
return true;
};
}
// 双向选择联动:列表行选中 ↔ VTK 足迹高亮。两向各自屏蔽回环(setSelectedMapLines 不回调、
// setSelectedDsIds 屏蔽信号),故无需额外守卫。
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::selectedDatasetsChanged, &window,
[sceneView](const QStringList& ids) {
std::vector<std::string> v;
for (const QString& s : ids) v.push_back(s.toStdString());
sceneView->setSelectedMapLines(v);
});
sceneView->onMapLineSelectionChanged = [sceneView, drawer]() {
QStringList ids;
for (const std::string& s : sceneView->selectedMapLines())
ids << QString::fromStdString(s);
drawer->col2D()->setSelectedDsIds(ids);
};
// 双向选择联动B2去 col2D 后由统一段 datasetSelected 承担 2D 选中,此处旧 col2D 链已移除)。
// 坐标轴设置抽屉面板:叠加 vtkWidget、工具条右侧滑出默认隐藏点设置 toggle
auto* axesPanel = new geopro::app::AxesSettingsPanel(vtkWidget);
@ -650,7 +637,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// (设计 §2.1:三维分析栏按 对象/三维体模型/切片 三级树),归三维分析栏(非数据集栏)。
// 后端列表每次勾选测线全量覆盖,故客户端体单独维护、刷新时合并注入(不被冲掉)。
// 三维分析数据源 = 最近对象树勾选拉取的 ds + 客户端三维体(mock) + 已保存切片;
// splitByCategory 后注入 5 段(电阻率/视电阻率/瞬变/三维体/切片);二维(足迹)经 dim2D 仍走 col2D
// splitByCategory 后注入各段(电阻率/视电阻率/瞬变/三维体/轨迹);二维(足迹/轨迹)并入同一单列(B2)
auto lastSourceRows = std::make_shared<std::vector<geopro::data::DsRow>>();
auto lastStructNodes = std::make_shared<std::vector<geopro::data::StructNode>>(); // 生成位置候选(项目内 GS/TM)
auto refreshAnalysis = [drawer, scene3dRepo, lastSourceRows, lastStructNodes]() {
@ -667,7 +654,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
for (const auto& s : slices) voxelTree.push_back(s);
for (const auto& a : anomalies) voxelTree.push_back(a);
if (auto* sec = drawer->analysisTab()->section("voxel")) sec->setDatasets(voxelTree);
drawer->col2D()->setDatasets(geopro::app::splitByDimension(*lastSourceRows).dim2D);
// B2二维(足迹/轨迹)不再走 col2DsplitByCategory 已含 trajectory 段,随 setBuckets 自动入单列。
drawer->analysisTab()->refreshVisibility(); // voxel 段单独喂数据后刷新分段可见性Task B1
};
// 渲染勾选聚合:三维数据集栏(剖面→帘面)+ 三维分析栏(三维体/切片→体素/切片)两套勾选并集
@ -678,10 +666,31 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 让「三维分析栏勾选(体/切片)」这条渲染路径也能隐藏不透明引导层——否则它盖住已渲染的体
// (雷达体由分析栏勾选触发渲染,但旧逻辑只在对象树勾选时隐藏引导层 → 体被盖住看不到)。
auto setSceneEmptyVisible = std::make_shared<std::function<void(bool)>>();
auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis, setSceneEmptyVisible]() {
// 勾选并集 → (dsId, typeId=描述符 id) 列表 → 控制器统一入口B2经描述符路由渲染策略无维度散判
// typeId 解析:三维体(mock不在 lastSourceRows) → "voxel";其余(剖面/轨迹)在 lastSourceRows
// 按 categoryCatalog().classify 命中描述符 → 其 id。控制器据 catalog[typeId].renderStrategyId 派策略。
auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis, setSceneEmptyVisible,
scene3dRepo, lastSourceRows]() {
QStringList all = *checkedProfiles;
all += *checkedAnalysis;
sceneCtrl->setCheckedDatasets(all);
std::vector<std::pair<std::string, std::string>> idType;
const auto& cat = geopro::data::categoryCatalog();
for (const QString& q : all) {
const std::string id = q.toStdString();
std::string typeId;
if (scene3dRepo->isVolumeDataset(id)) {
typeId = "voxel"; // 客户端三维体(mock) 不在 lastSourceRows按仓储谓词直判
} else {
const auto& rows = *lastSourceRows; // 剖面/轨迹按描述符 classify 解析具体类型
auto it = std::find_if(rows.begin(), rows.end(),
[&](const geopro::data::DsRow& r) { return r.id == id; });
if (it != rows.end())
for (const auto& d : cat)
if (d.classify && d.classify(*it)) { typeId = d.id; break; }
}
if (!typeId.empty()) idType.push_back({id, typeId});
}
sceneCtrl->setCheckedDatasets(idType);
if (*setSceneEmptyVisible) (*setSceneEmptyVisible)(all.isEmpty()); // 场景有内容→隐藏引导层
};
@ -1385,48 +1394,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 当前底图选择(默认 天地图=Satellite对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。
auto basemapKind =
std::make_shared<geopro::app::TileBasemap::Kind>(geopro::app::TileBasemap::Satellite);
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::basemapChanged, basemap,
[basemap, basemapKind](int idx) {
// 地图下拉0 天地图(卫星影像) / 1 Google(暂未实现→隐藏) / 2 隐藏。
*basemapKind = (idx == 0) ? geopro::app::TileBasemap::Satellite
: geopro::app::TileBasemap::Hidden;
basemap->show(*basemapKind);
});
// ── 二维数据集栏:勾选足迹(测线/轨迹) → 平铺进 View3D 地图2D视图下拉控摆放高度 ──
// 足迹经控制器 loadMapLine(Api3dRepository 走 dd/ert/trajectory/line 端点) → addMapLine 至
// 当前摆放 Z与帘面/底图共享 GeoLocalFrame 配准。与 3D 勾选集独立、按 dsId 增量。
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::checkedDatasetsChanged,
sceneCtrl, &geopro::controller::VtkSceneController::setChecked2DDatasets);
// 2D视图下拉(0关闭/1 Z=0/2顶部/3底部/4自定义) + 自定义 Z → 控制器重摆已勾选足迹。
auto custom2dZ = std::make_shared<double>(0.0);
// 默认 1(Z=0)与「2D视图」下拉可见默认项(setCurrentIndex(1))及控制器默认摆放一致——
// 组合框初始 view2DModeChanged 因 connect 前发射而丢失,此处不可依赖其同步初值。
auto view2dMode = std::make_shared<int>(1);
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::view2DModeChanged, sceneCtrl,
[sceneCtrl, custom2dZ, view2dMode](int mode) {
*view2dMode = mode;
sceneCtrl->set2DPlacement(mode, *custom2dZ);
});
// 自定义 Z 变化:记录;若当前正处自定义模式则即时重摆(控制器内 changed 判定避免无谓重画)。
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::customZChanged, sceneCtrl,
[sceneCtrl, custom2dZ, view2dMode](double z) {
*custom2dZ = z;
if (*view2dMode == 4) sceneCtrl->set2DPlacement(4, z);
});
// ── 二维分析改造 A 期:切「三维分析/二维分析」tab → 一场景两相机 ──────────────────
// 三处协作:①切片隐藏+交互锁(仅平移+缩放) [InteractionManager];②按目标维度重置取景基线
// [VtkSceneController]——使切换后该维度首条数据自动取景;③维度显隐+近俯视/自由相机+取景+坐标轴+
// 渲染 [VtkSceneView]。顺序:先 ①②(都不渲染),最后 ③ 收尾统一渲染。只翻可见标志、不清空/重建 →
// 切换瞬时;地形+底图常驻。
QObject::connect(drawer, &geopro::app::ColumnDrawer::analysisModeChanged, &window,
[interactionMgr, sceneCtrl, sceneView, viewToolbar, refreshAlongLineBar](bool is2D) {
interactionMgr->setMode2D(is2D);
sceneCtrl->onAnalysisModeChanged(is2D);
sceneView->setAnalysisMode2D(is2D);
viewToolbar->setAnalysisMode2D(is2D); // 二维下禁用 6 向快捷视图
(*refreshAlongLineBar)(); // 二维隐藏沿线滑块、三维细长体显示(B#2)
});
// B2col2D 已删 —— 旧 2D 面板信号basemapChanged / checkedDatasetsChanged / view2DModeChanged /
// customZChanged接线随之移除。其替代工具条 3D 底图、平面 z 滑块、2D 底图弹层)由 Phase D3/E3/F2
// 重接。轨迹(足迹)勾选现经统一段 checkedDatasetsChanged → pushChecked → "plane2d" 策略渲染。
// 底图默认仍为天地图(basemapKind=Satellite),首个数据重锚后由 onFrameReanchored 显示。
// 抽屉去 tab 后无「三维/二维分析」切换,视图固定三维分析;旧 analysisModeChanged 接线移除C2/D3 重接)。
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
@ -1701,7 +1673,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
emptyState->setVisible(sources.isEmpty() && checkedAnalysis->isEmpty());
if (sources.isEmpty()) {
*lastSourceRows = {};
refreshAnalysis(); // 清空 5 段(客户端三维体仍驻留) + col2D
refreshAnalysis(); // 清空各段(客户端三维体仍驻留)
return;
}
// 多源异步汇总:每个源(TM / GS·项目根直挂)按 confType 取整棵 ds 子树,全部回来后 splitByCategory 分 5 段。
@ -1710,7 +1682,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto finish = [acc, generation, myGen, lastSourceRows, refreshAnalysis]() {
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
*lastSourceRows = *acc; // 全部对象树 ds 作分析数据源
refreshAnalysis(); // splitByCategory→5段 + 合并三维体/切片 + dim2D→col2D
refreshAnalysis(); // splitByCategory→各段 + 合并三维体/切片(单列)
};
for (const geopro::data::DataSource& src : sources) {
// 第3参 confType1=GS/项目根(直挂 ds)2=TM(测线下 ds)——透传给 loadRowsAsync(spec §6)。
@ -1802,21 +1774,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 切换到「不同项目」时先清空中央区,避免新项目残留旧项目的三栏数据与 VTK 渲染。
// 仅真正换项目用delete-refresh 等 switchProject(currentProjectId) 不走此处,避免误清)。
auto clearCentral = [sceneCtrl, emptyState, checkedProfiles, checkedAnalysis,
auto clearCentral = [emptyState, checkedProfiles, checkedAnalysis,
pushChecked, lastSourceRows, refreshAnalysis, checkedSliceIds,
syncSlices, basemap, sceneView, scene3dRepo]() {
// 切项目:先清内存态三维体/切片/异常(否则上个项目的产物残留进新项目列表),再 refreshAnalysis。
scene3dRepo->clearMockData();
// 数据源清空 → 5 段 + col2D 清空refreshAnalysis 内 setBuckets/dim2D)。
// 数据源清空 → 各段清空refreshAnalysis 内 setBuckets含 trajectory 段)。
*lastSourceRows = {};
refreshAnalysis();
// 勾选集清空并下发空到 VTK帘面/体素/切片/2D 足迹全部撤场)。
// 勾选集清空并下发空到 VTK帘面/体素/切片/2D 足迹全部撤场——统一入口一并清)。
checkedProfiles->clear();
checkedAnalysis->clear();
checkedSliceIds->clear();
pushChecked(); // setCheckedDatasets({}) → 帘面/体素清空
syncSlices(); // 切片随空勾选调和
sceneCtrl->setChecked2DDatasets({}); // 2D 足迹显式撤场(与 col2D 空勾选双保险)
pushChecked(); // setCheckedDatasets({}) → 帘面/体素/足迹清空(统一勾选集 diff 全移除)
syncSlices(); // 切片随空勾选调和
// 复位重锚标志:增量勾选不走 clear(),不复位则旧标志残留 → 新项目数据不重锚 →
// onFrameReanchored 不触发 → 下面 hide() 的底图永不再显。复位后新项目首个数据重锚→重显。
sceneView->resetFrameAnchor();

View File

@ -1,34 +1,19 @@
#include "panels/columns/ColumnDrawer.hpp"
#include "panels/columns/Column2DDataset.hpp"
#include "panels/columns/CategoryAnalysisTab.hpp"
#include <algorithm>
#include "Glyphs.hpp"
#include "Theme.hpp"
#include <QHBoxLayout>
#include <QPushButton>
#include <QResizeEvent>
#include <QTabBar>
#include <QTabWidget>
namespace geopro::app {
ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary* dict)
: QWidget(parent)
{
col2D_ = new Column2DDataset(this);
// 单列承载:去 QTabWidgetbody_ 直接为 CategoryAnalysisTab含 trajectory 段,二维并入同列)。
analysisTab_ = new CategoryAnalysisTab(dict, this);
// Tab 容器body_两 tab三维分析[分段] / 二维分析)。
auto* tabs = new QTabWidget(this);
body_ = tabs;
tabs->addTab(analysisTab_, QStringLiteral("三维分析"));
tabs->addTab(col2D_, QStringLiteral("二维分析"));
tabs->tabBar()->setUsesScrollButtons(false); // 永不出左右滚动箭头(两 tab 必能平铺)
// 切 tab → 发 analysisModeChanged(is2D):以"当前 widget 是否 col2D"判定,不写死索引。
connect(tabs, &QTabWidget::currentChanged, this, [this, tabs](int idx) {
emit analysisModeChanged(tabs->widget(idx) == col2D_);
});
body_ = analysisTab_;
// 折叠按钮:固定宽 18px垂直拉伸。
// 用 SVG 图标(makeGlyph)而非 ◀/▶ 文字——三角符(U+25C0/25B6)不在 YaHei作按钮文字会触发
@ -58,22 +43,6 @@ ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary
setMaximumWidth(560);
}
void ColumnDrawer::resizeEvent(QResizeEvent* e)
{
QWidget::resizeEvent(e);
// 两 tab 平分抽屉宽度填满(带样式表的 tab 不响应 setExpanding须按 barWidth/n 显式给宽)。
// 消除旧 3 栏布局遗留的右侧空白——重构成 2 栏后不再三分、留空第三位。
if (auto* tabs = qobject_cast<QTabWidget*>(body_)) {
const int n = tabs->count();
if (n > 0 && tabs->width() > 0) {
// 每 tab 内容宽 = 总宽/n - 每 tab 非内容开销(全局 QSS padding 8+16+16=… 约 32 + margin 4)。
// 稍欠一点宽避免溢出(溢出会触发滚动箭头)setUsesScrollButtons(false) 再兜底。
const int w = std::max(40, tabs->width() / n - 42);
tabs->tabBar()->setStyleSheet(QStringLiteral("QTabBar::tab{width:%1px;}").arg(w));
}
}
}
void ColumnDrawer::toggleCollapsed()
{
collapsed_ = !collapsed_;

View File

@ -2,7 +2,6 @@
#include <QWidget>
class QPushButton;
class QResizeEvent;
namespace geopro::data {
class DatasetFieldDictionary;
@ -10,36 +9,29 @@ class DatasetFieldDictionary;
namespace geopro::app {
class Column2DDataset;
class CategoryAnalysisTab;
// VTK视图左侧内嵌抽屉两 tab(三维分析[按数据类型分段]/二维分析) + 折叠开关。
// VTK视图左侧内嵌抽屉单列承载 CategoryAnalysisTab(按数据类型分段,含 trajectory 段) + 折叠开关。
// B2 去 tab原「三维分析 / 二维分析」双 tab 合一,二维(足迹/轨迹)经描述符分段并入同一列。
class ColumnDrawer : public QWidget {
Q_OBJECT
public:
explicit ColumnDrawer(QWidget* parent = nullptr,
geopro::data::DatasetFieldDictionary* dict = nullptr);
Column2DDataset* col2D() const { return col2D_; }
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
signals:
// 切换「三维分析 / 二维分析」tabis2D=true 进入二维分析。上层据此切相机+翻维度可见标志。
void analysisModeChanged(bool is2D);
// 折叠/展开切换:上层据此调整 QSplitter 尺寸,回收/恢复抽屉栏宽(否则收起残留空白区)。
void collapsedChanged(bool collapsed);
public slots:
void toggleCollapsed();
void expand(); // 强制展开(进入全屏时确保三栏可见)
protected:
void resizeEvent(QResizeEvent* e) override; // 两 tab 按抽屉宽平分(消除右侧空白"第三栏位"
void expand(); // 强制展开(进入全屏时确保单列可见)
private:
Column2DDataset* col2D_ = nullptr;
CategoryAnalysisTab* analysisTab_ = nullptr;
QWidget* body_ = nullptr; // QTabWidget折叠时隐藏
QWidget* body_ = nullptr; // = analysisTab_折叠时隐藏
QPushButton* toggleBtn_ = nullptr;
bool collapsed_ = false;
};

View File

@ -1,15 +1,24 @@
#include "controller/DatasetRenderStrategy.hpp"
#include "controller/VtkSceneController.hpp" // 完整类型 + 含 I3dSceneViewview_.removeDataset
namespace geopro::controller {
// 骨架实现渲染体由后续阶段填充Phase B / E / F。当前为空实现仅保证链接。
void VolumeRenderStrategy::add(const std::string& /*typeId*/, const std::string& /*dsId*/) {}
void VolumeRenderStrategy::remove(const std::string& /*dsId*/) {}
// 各策略委托回控制器既有渲染路径(友元访问其私有 addDatasetAsync/add2DDatasetAsync/view_
// 增量 gen 沿用控制器当前 rebuildGeneration_不自增与并发增量互不作废
void VolumeRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
ctrl_.addDatasetAsync(dsId, ctrl_.rebuildGeneration_);
}
void VolumeRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
void CurtainRenderStrategy::add(const std::string& /*typeId*/, const std::string& /*dsId*/) {}
void CurtainRenderStrategy::remove(const std::string& /*dsId*/) {}
void CurtainRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
ctrl_.addDatasetAsync(dsId, ctrl_.rebuildGeneration_);
}
void CurtainRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
void Plane2DRenderStrategy::add(const std::string& /*typeId*/, const std::string& /*dsId*/) {}
void Plane2DRenderStrategy::remove(const std::string& /*dsId*/) {}
void Plane2DRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
ctrl_.add2DDatasetAsync(dsId, ctrl_.rebuildGeneration_);
}
void Plane2DRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
} // namespace geopro::controller

View File

@ -5,6 +5,8 @@
namespace geopro::controller {
class VtkSceneController; // 策略委托回控制器既有渲染路径add→addDatasetAsync/add2DDatasetAsyncremove→view.removeDataset
class IDatasetRenderStrategy {
public:
virtual ~IDatasetRenderStrategy() = default;
@ -27,26 +29,34 @@ private:
std::map<std::string, std::unique_ptr<IDatasetRenderStrategy>> strategies_;
};
// 3 骨架策略本任务仅建类与注册。add/remove 的真实渲染实现由后续阶段填充——
// Phase BVolumeRenderStrategy/CurtainRenderStrategy 包现有 3D 分支)与
// Phase E/FPlane2DRenderStrategy 平面 z + 底图)。届时各自接所需的
// VtkSceneController& / View / Repo 引用。当前 .cpp 给空实现使链接通过。
// 3 策略:各持 VtkSceneController& 引用,把 add/remove 委托回控制器既有渲染路径。
// Volume/Curtain::add 均转调 addDatasetAsync其内部按 isVolumeDataset 自分体/帘面分支);
// Plane2D::add 暂转调 add2DDatasetAsyncPhase E/F 改平面 z + 底图。remove 统一 view.removeDataset。
class VolumeRenderStrategy : public IDatasetRenderStrategy {
public:
explicit VolumeRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
private:
VtkSceneController& ctrl_;
};
class CurtainRenderStrategy : public IDatasetRenderStrategy {
public:
explicit CurtainRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
private:
VtkSceneController& ctrl_;
};
class Plane2DRenderStrategy : public IDatasetRenderStrategy {
public:
explicit Plane2DRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
private:
VtkSceneController& ctrl_;
};
} // namespace geopro::controller

View File

@ -9,6 +9,8 @@
#include "DatasetViewState.hpp"
#include "I3dSceneView.hpp"
#include "controller/DatasetRenderStrategy.hpp"
#include "repo/CategoryDescriptor.hpp"
#include "repo/IDatasetRepository.hpp"
namespace geopro::controller {
@ -23,7 +25,19 @@ constexpr double kBottomOffsetZ = -50.0; // 底部:参考面下方
VtkSceneController::VtkSceneController(data::IDatasetRepository& dsRepo,
data::I3dSceneRepository& sceneRepo, I3dSceneView& view,
QObject* parent)
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {}
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {
// 注册 3 渲染策略(键与 CategoryDescriptor.renderStrategyId 对应)。各持本控制器引用,
// add/remove 委托回既有渲染路径addDatasetAsync/add2DDatasetAsync/view_.removeDataset
registry_.registerStrategy("volume", std::make_unique<VolumeRenderStrategy>(*this));
registry_.registerStrategy("curtain", std::make_unique<CurtainRenderStrategy>(*this));
registry_.registerStrategy("plane2d", std::make_unique<Plane2DRenderStrategy>(*this));
}
IDatasetRenderStrategy* VtkSceneController::strategyForType(const std::string& typeId) const {
for (const auto& d : geopro::data::categoryCatalog())
if (d.id == typeId) return registry_.get(d.renderStrategyId);
return nullptr;
}
void VtkSceneController::setViewState(DatasetViewState* state) {
state_ = state;
@ -58,84 +72,41 @@ void VtkSceneController::recolorDataset(const QString& qid) {
if (changed) view_.renderIncremental();
}
void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
std::vector<std::string> newDs;
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
for (const QString& id : dsIds) newDs.push_back(id.toStdString());
void VtkSceneController::setCheckedDatasets(
const std::vector<std::pair<std::string, std::string>>& idType) {
std::map<std::string, std::string> next; // dsId→typeId
for (const auto& p : idType) next[p.first] = p.second;
// 2D 俯视测线:保持全量重建(测线非按 ds 跟踪移除)。
if (mode_ == ViewMode::Map2D) {
checkedDs_ = std::move(newDs);
rebuildInternal();
return;
}
const std::map<std::string, std::string> prev = checked_; // diff 快照(区分新增/移除)
// 3D增量 diff —— 只处理新增/移除,不全量重建(底图、其余 ds、相机均不动
const std::set<std::string> oldSet(checkedDs_.begin(), checkedDs_.end());
const std::set<std::string> newSet(newDs.begin(), newDs.end());
// 移除:旧有新无 → 派该 ds 类型策略 remove活跃计数归零则 onTypeDeactivated。
for (const auto& [id, typeId] : prev)
if (!next.count(id)) {
if (auto* s = strategyForType(typeId)) s->remove(id);
if (--typeActive_[typeId] == 0)
if (auto* s = strategyForType(typeId)) s->onTypeDeactivated(typeId);
}
for (const auto& id : checkedDs_)
if (!newSet.count(id)) view_.removeDataset(id); // 移除:旧有新无 → 仅删该 ds 图元
checkedDs_ = std::move(newDs);
// 取景意图按「场景是否已有数据到场过」判定,而非 checkedDs_ 是否空——否则连续快速勾选第二个
// ds 时 checkedDs_ 已非空但首批尚未到场,会被误清取景意图,相机不对准数据 → 看似不渲染。
// 取景意图按「场景是否已有数据到场过」判定(连续快速勾选时 checked_ 已非空但首批未到场,
// 不可据 checked_ 空否清取景意图,否则相机不对准数据 → 看似不渲染)。全消时复位基线。
fitOnArrival_ = !hadArrivedData_;
// 仅当 3D 与 2D 都空才复位取景基线:否则取消全部 3D 但仍有 2D 足迹时,下个 3D 勾选会误跳取景。
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
if (next.empty()) hadArrivedData_ = false;
const unsigned long long gen = rebuildGeneration_; // 不自增:并发增量互不作废
for (const auto& id : checkedDs_)
if (!oldSet.count(id)) addDatasetAsync(id, gen); // 新增:新有旧无 → 异步取数增量入场
// 先提交勾选集,再派 add异步取数回调以 isChecked() 守「仍勾选?」,同步仓储(测试)会即时回灌,
// 若 add 时 checked_ 未更新则 isChecked 假、回调丢弃 → 不渲染。故必须先 commit 再 add。
checked_ = next;
// 新增:新有旧无 → 活跃计数 0→1 时 onTypeActivated再派策略 add委托回既有渲染路径
for (const auto& [id, typeId] : checked_)
if (!prev.count(id)) {
if (typeActive_[typeId]++ == 0)
if (auto* s = strategyForType(typeId)) s->onTypeActivated(typeId);
if (auto* s = strategyForType(typeId)) s->add(typeId, id);
}
view_.renderIncremental(); // 立即反映移除 / 触发坐标轴重算
}
void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) {
std::vector<std::string> newDs;
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
for (const QString& id : dsIds) newDs.push_back(id.toStdString());
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff不全量重建不打断 3D 帘面/体)。
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
const std::set<std::string> newSet(newDs.begin(), newDs.end());
for (const auto& id : checked2dDs_)
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
checked2dDs_ = std::move(newDs);
// 取景基线与 3D 路径统一用 hadArrivedData_而非"两栏皆空"):否则二维分析下若已有隐藏的 3D 数据,
// 勾选首条足迹会因 wasEmpty=false 而不取景 → 足迹落在视野外。切 tab 时 onAnalysisModeChanged 已按
// 目标维度是否有数据重置该基线,故此处首条可见维度数据能正确取景。
fitOnArrival_ = !hadArrivedData_;
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
// 足迹画进 View3D 场景mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
const unsigned long long gen = rebuildGeneration_; // 不自增:与 3D 增量互不作废
for (const auto& id : checked2dDs_)
if (!oldSet.count(id)) add2DDatasetAsync(id, gen); // 新增 → 异步取足迹增量入场
}
view_.renderIncremental(); // 立即反映移除
}
void VtkSceneController::set2DPlacement(int mode, double customZ) {
const bool changed = (mode != placement2dMode_) || (mode == 4 && customZ != customZ2d_);
placement2dMode_ = mode;
customZ2d_ = customZ;
if (!changed || checked2dDs_.empty()) return;
// 摆放变化 → 对已勾选足迹重摆:先全部移除,再按新 Z 重加mode=0 关闭则只移除不重加)。
for (const auto& id : checked2dDs_) view_.removeDataset(id);
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
const unsigned long long gen = rebuildGeneration_;
fitOnArrival_ = false; // 重摆:保持相机
for (const auto& id : checked2dDs_) add2DDatasetAsync(id, gen);
}
view_.renderIncremental();
}
double VtkSceneController::placementZ() const {
const double surf = view_.zRefElev(); // 真实地表高程基准(测线地表高程)
switch (placement2dMode_) {
@ -260,11 +231,11 @@ void VtkSceneController::onDatasetArrived() {
}
bool VtkSceneController::isChecked(const std::string& dsId) const {
return std::find(checkedDs_.begin(), checkedDs_.end(), dsId) != checkedDs_.end();
return checked_.count(dsId) > 0; // 统一勾选集(异步回调「仍勾选?」守护)
}
bool VtkSceneController::is2DChecked(const std::string& dsId) const {
return std::find(checked2dDs_.begin(), checked2dDs_.end(), dsId) != checked2dDs_.end();
return checked_.count(dsId) > 0; // 同上2D 足迹与 3D 同处统一勾选集
}
void VtkSceneController::setViewMode(ViewMode mode) {
@ -277,7 +248,9 @@ void VtkSceneController::onAnalysisModeChanged(bool is2D) {
// 目标维度空 → hadArrivedData_=false切换后该维度第一条数据自动取景(治"3D 数据不知生成到哪")。
// 目标维度非空 → hadArrivedData_=true视图切换时已 fit 到该维度,后续勾选不再跳(与三维一致)。
// 显隐/相机/坐标轴由 VtkSceneView::setAnalysisMode2D 处理(上层在同一处调用);此处只管取景基线。
hadArrivedData_ = is2D ? !checked2dDs_.empty() : !checkedDs_.empty();
// 注B2 重构后抽屉去 tab、analysisModeChanged 接线移除,本槽暂无调用方(保留待 C2/D3 复用)。
(void)is2D;
hadArrivedData_ = !checked_.empty();
}
void VtkSceneController::setLayer(SceneLayer layer, bool on) {
@ -393,7 +366,7 @@ void VtkSceneController::rebuildInternal() {
// 坏 dsIdloadGrid/loadColorScale 抛异常)= best-effort 跳过emit loadFailed 但不中断。
try {
if (is2D) {
for (const auto& dsId : checkedDs_) view_.addSurveyLine(grid(dsId));
for (const auto& [dsId, typeId] : checked_) view_.addSurveyLine(grid(dsId));
} else {
// 回调用 QPointer<self> 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。
QPointer<VtkSceneController> self(this);
@ -409,10 +382,10 @@ void VtkSceneController::rebuildInternal() {
emit self->loadFailed(QString::fromStdString(m));
});
}
for (const auto& dsId : checkedDs_) addDatasetAsync(dsId, gen);
// 二维足迹随全量重建一并重画clear 已移除其图元mode=0 关闭则跳过
if (placement2dMode_ != 0)
for (const auto& dsId : checked2dDs_) add2DDatasetAsync(dsId, gen);
// 全量重建clear 已移除全部图元,据统一勾选集经各 ds 类型策略重放 add不动活跃计数
// 已在 setCheckedDatasets 计入;策略 add 内部转调 addDatasetAsync/add2DDatasetAsync
for (const auto& [dsId, typeId] : checked_)
if (auto* s = strategyForType(typeId)) s->add(typeId, dsId);
}
} catch (const std::exception& e) {
emit loadFailed(QString::fromStdString(e.what()));

View File

@ -7,8 +7,11 @@
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "I3dSceneView.hpp"
#include "controller/DatasetRenderStrategy.hpp"
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/I3dSceneRepository.hpp"
@ -34,6 +37,10 @@ enum class SceneLayer { Curtain, Voxel, Terrain };
// 不持有 widget不认 vtkActor/vtkVolume全交给 I3dSceneView
class VtkSceneController : public QObject {
Q_OBJECT
// 渲染策略委托回控制器既有路径addDatasetAsync/add2DDatasetAsync/view_友元免widen公有面。
friend class VolumeRenderStrategy;
friend class CurtainRenderStrategy;
friend class Plane2DRenderStrategy;
public:
VtkSceneController(data::IDatasetRepository& dsRepo, data::I3dSceneRepository& sceneRepo,
I3dSceneView& view, QObject* parent = nullptr);
@ -42,12 +49,13 @@ public:
// 构造后由 main.cpp 注入一次。
void setViewState(DatasetViewState* state);
public:
// 勾选并集统一入口(取代旧 setCheckedDatasets(QStringList)/setChecked2DDatasets
// 每项 = (dsId, typeId=描述符 id)。diff vs 上次后按 catalog[typeId].renderStrategyId 派给策略
// add/remove并维护「每 typeId 活跃数」在首勾/全消时调 onTypeActivated/Deactivated。
void setCheckedDatasets(const std::vector<std::pair<std::string, std::string>>& idType);
public slots:
void setCheckedDatasets(const QStringList& dsIds);
// 二维数据集栏勾选(足迹型测线/轨迹)→ 平铺进 View3D 地图。与 3D 勾选集独立,按 dsId 增量。
void setChecked2DDatasets(const QStringList& dsIds);
// 二维足迹摆放高度mode0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义customZ 仅 mode=4 用)。
void set2DPlacement(int mode, double customZ);
void setViewMode(ViewMode mode);
// 切「三维分析/二维分析」tabA 期):按目标维度是否已有数据重置取景基线,使切换后该维度第一条
// 数据自动取景。显隐/相机/坐标轴由 VtkSceneView::setAnalysisMode2D 负责(上层在同一处一并调用)。
@ -96,6 +104,8 @@ private:
void onDatasetArrived(); // 单个 ds 落地后:增量渲染 + 首批数据自动取景
bool isChecked(const std::string& dsId) const;
bool is2DChecked(const std::string& dsId) const;
// 按 typeId 查其渲染策略catalog[typeId].renderStrategyId → registry_。未知 typeId 返回 nullptr。
IDatasetRenderStrategy* strategyForType(const std::string& typeId) const;
// 当前摆放模式下足迹的世界 Zmode 0=关闭由调用方拦截;此处算 1/2/3/4 的 Z
double placementZ() const;
@ -103,13 +113,14 @@ private:
data::I3dSceneRepository& sceneRepo_;
I3dSceneView& view_;
std::vector<std::string> checkedDs_;
// 二维足迹勾选集(与 checkedDs_ 独立;都画进 View3D 场景)。按 dsId 增量加/删。
std::vector<std::string> checked2dDs_;
// 统一勾选集2D+3D 合一dsId→typeId(描述符 id)。增量 diff 的真源rebuildInternal 据此重放。
std::map<std::string, std::string> checked_;
// 每 typeId 活跃计数:首勾(0→1)调 onTypeActivated、全消(1→0)调 onTypeDeactivated。
std::map<std::string, int> typeActive_;
// 渲染策略注册表(构造时注册 volume/curtain/plane2d 三策略,各持本控制器引用)。
RenderStrategyRegistry registry_;
// 二维足迹摆放mode 0关闭/1 Z=0/2顶部/3底部/4自定义customZ2d_ 仅 mode=4 用。
// 默认 Z=0(1) 与 Column2DDataset「2D视图」下拉可见默认项一致——避免「下拉显示 Z=0 但
// 控制器实为关闭」的初始信号丢失desync(组合框 setCurrentIndex 在 connect 前发射、且
// 组件早于 main.cpp 接线构造,初始 view2DModeChanged 永不送达),致勾选足迹静默不渲染。
// 默认 Z=0(1);摆放 UI平面 z 滑块/底图)由 Phase E/F 重接,当前固定默认。
int placement2dMode_ = 1;
double customZ2d_ = 0.0;
ViewMode mode_ = ViewMode::Map2D;

View File

@ -203,26 +203,27 @@ struct FakeSceneRepo : data::I3dSceneRepository {
} // 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);
// 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 模式 + 帘面图层 → 1 帘面 actor。
// 3D 帘面:勾选电阻率(curtain 策略) → 1 帘面 actor。
TEST(VtkSceneController, View3DCurtainAddsCurtain) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(curtainIds({"ds1"}));
EXPECT_EQ(view.curtains, 1);
EXPECT_EQ(view.surveyLines, 0);
@ -235,7 +236,7 @@ TEST(VtkSceneController, View3DVolumeDatasetAddsVolume) {
sc.volumeIds = {"ds1"}; // ds1 = 三维体类型 → 体素渲染路径
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(voxelIds({"ds1"}));
EXPECT_EQ(view.volumes, 1);
EXPECT_EQ(view.curtains, 0); // 体素数据集不再同时出帘面
@ -247,18 +248,18 @@ TEST(VtkSceneController, View3DWithTerrainAddsTerrain) {
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setLayer(SceneLayer::Terrain, true);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(curtainIds({"ds1"}));
EXPECT_EQ(view.terrains, 1);
EXPECT_EQ(view.curtains, 1);
}
// 取消勾选 → 增量移除该 ds 图元(不整场 clear3D 增量路径)。
// 取消勾选 → 增量移除该 ds 图元(不整场 clear增量路径)。
TEST(VtkSceneController, UncheckRemovesDatasetIncrementally) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(curtainIds({"ds1"}));
ASSERT_EQ(view.curtains, 1);
const int clearsAfterCheck = view.clears;
@ -273,11 +274,11 @@ TEST(VtkSceneController, IncrementalAddKeepsExisting) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(curtainIds({"ds1"}));
const int clearsAfterFirst = view.clears;
ASSERT_EQ(view.curtains, 1);
c.setCheckedDatasets({"ds1", "ds2"}); // 增量加 ds2
c.setCheckedDatasets(curtainIds({"ds1", "ds2"})); // 增量加 ds2
EXPECT_EQ(view.curtains, 2);
EXPECT_EQ(view.clears, clearsAfterFirst); // 不重建 → 无新 clear
}
@ -288,7 +289,7 @@ TEST(VtkSceneController, VerticalExaggerationForwarded) {
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setVerticalExaggeration(3.5);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(curtainIds({"ds1"}));
EXPECT_DOUBLE_EQ(view.ve, 3.5);
}
@ -297,7 +298,7 @@ TEST(VtkSceneController, MultipleDatasetsAddMultipleCurtains) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1", "ds2", "ds3"});
c.setCheckedDatasets(curtainIds({"ds1", "ds2", "ds3"}));
EXPECT_EQ(view.curtains, 3);
}
@ -309,7 +310,7 @@ TEST(VtkSceneController, SetVolumeColorScaleRebuildsCheckedVolume) {
sc.volumeIds = {"ds1"};
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1"});
c.setCheckedDatasets(voxelIds({"ds1"}));
ASSERT_EQ(view.volumes, 1);
const int removesBefore = view.removeCalls;
@ -330,7 +331,7 @@ TEST(VtkSceneController, SetVolumeColorScalePersistsAcrossRecheck) {
sc.volumeIds = {"ds1"};
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"ds1"}); // 加载体(填充 volumeCache_
c.setCheckedDatasets(voxelIds({"ds1"})); // 加载体(填充 volumeCache_
ASSERT_EQ(view.lastVolumeScale.stopCount(), 2u); // 初始两段
core::ColorScale edited; // 编辑成三段
@ -340,8 +341,8 @@ TEST(VtkSceneController, SetVolumeColorScalePersistsAcrossRecheck) {
c.setVolumeColorScale("ds1", edited);
ASSERT_EQ(view.lastVolumeScale.stopCount(), 3u);
c.setCheckedDatasets({}); // 取消勾选
c.setCheckedDatasets({"ds1"}); // 再勾选 → 命中缓存(含编辑后色阶)
c.setCheckedDatasets({}); // 取消勾选
c.setCheckedDatasets(voxelIds({"ds1"})); // 再勾选 → 命中缓存(含编辑后色阶)
EXPECT_EQ(view.lastVolumeScale.stopCount(), 3u);
}
@ -409,146 +410,56 @@ TEST(VtkSceneController, ZoomAndFitForwarded) {
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);
// ── 二维数据集(轨迹/足迹)经 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
// 回归(足迹默认不渲染 bug):默认摆放=Z=0(1),与 2D视图下拉可见默认项一致 →
// 仅勾选 2D 足迹(不手动调 set2DPlacement即应在 View3D 渲染worldZ=0。
TEST(VtkSceneController, TwoDDefaultPlacementRendersAtZeroOnCheck) {
// 勾选轨迹(plane2d 策略) → 1 条 mapLine默认摆放 worldZ=0不影响帘面/体素计数。
TEST(VtkSceneController, TrajectoryRendersAsMapLineAtDefaultZero) {
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 条 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"});
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);
}
// 顶部/底部摆放锚定真实地表高程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) {
// 取消勾选轨迹 → 增量移除该足迹图元(不整场 clear
TEST(VtkSceneController, TrajectoryUncheckRemovesMapLine) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.set2DPlacement(1, 0.0);
c.setChecked2DDatasets({"traj1"});
c.setCheckedDatasets(trajIds({"traj1"}));
ASSERT_EQ(view.mapLines, 1);
const int clearsBefore = view.clears;
c.setChecked2DDatasets({}); // 取消勾选
c.setCheckedDatasets({}); // 取消勾选
EXPECT_EQ(view.mapLines, 0);
EXPECT_EQ(view.clears, clearsBefore); // 增量移除,不整场 clear
}
// 2D 足迹与 3D 帘面共存且独立:勾选剖面 + 足迹,各出各的图元,互不影响
TEST(VtkSceneController, TwoDFootprintCoexistsWith3DCurtain) {
// 轨迹足迹与 3D 帘面经同一入口共存且独立:各出各的图元,取消足迹不影响帘面。
TEST(VtkSceneController, TrajectoryCoexistsWith3DCurtain) {
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 足迹
IdType both = curtainIds({"prof1"});
both.push_back({"traj1", "trajectory"});
c.setCheckedDatasets(both); // 帘面 + 足迹(统一入口并集)
EXPECT_EQ(view.curtains, 1);
EXPECT_EQ(view.mapLines, 1);
c.setChecked2DDatasets({}); // 取消足迹 → 帘面不受影响
c.setCheckedDatasets(curtainIds({"prof1"})); // 仅留帘面 → 足迹移除
EXPECT_EQ(view.mapLines, 0);
EXPECT_EQ(view.curtains, 1);
}
// 回归(BUG3二维分析切回三维分析后三维数据"不知生成到哪",要手动适配才定位)
// 二维勾选足迹自动取景后 hadArrivedData_=true切回三维前 onAnalysisModeChanged(false) 按"三维栏空"
// 复位取景基线 → 勾选三维数据应自动取景(fitView),而非停在旧相机。
TEST(VtkSceneController, ThreeDDataFitsAfterSwitchingBackFrom2D) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.onAnalysisModeChanged(true); // 切到二维(2D 栏空 → 基线允许取景)
c.setChecked2DDatasets({"traj1"});
ASSERT_EQ(view.mapLines, 1);
const int fitsAfter2D = view.fitCalls;
EXPECT_GE(fitsAfter2D, 1); // 足迹首次到场已取景
c.onAnalysisModeChanged(false); // 切回三维(3D 栏空 → 基线允许取景)
c.setCheckedDatasets({"prof1"});
EXPECT_EQ(view.curtains, 1);
EXPECT_GT(view.fitCalls, fitsAfter2D); // 三维数据到场自动取景(修复前不取景)
}
// 回归(二维分析下已有隐藏 3D 数据时,勾选首条足迹也应取景;旧 wasEmpty 逻辑因 3D 非空而漏取景)
TEST(VtkSceneController, TwoDFootprintFitsEvenWhenHidden3DExists) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setCheckedDatasets({"prof1"}); // 三维数据(取景一次)
const int fitsAfter3D = view.fitCalls;
c.onAnalysisModeChanged(true); // 切到二维(2D 栏空 → 基线允许取景)
c.setChecked2DDatasets({"traj1"});
EXPECT_EQ(view.mapLines, 1);
EXPECT_GT(view.fitCalls, fitsAfter3D); // 首条足迹取景(旧逻辑因有隐藏 3D 而漏)
}
// 自定义摆放(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);
}