diff --git a/src/app/main.cpp b/src/app/main.cpp index 9d3a598..a72fb18 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -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 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>(); auto lastStructNodes = std::make_shared>(); // 生成位置候选(项目内 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:二维(足迹/轨迹)不再走 col2D;splitByCategory 已含 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>(); - 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> 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::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(0.0); - // 默认 1(Z=0):与「2D视图」下拉可见默认项(setCurrentIndex(1))及控制器默认摆放一致—— - // 组合框初始 view2DModeChanged 因 connect 前发射而丢失,此处不可依赖其同步初值。 - auto view2dMode = std::make_shared(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) - }); + // B2:col2D 已删 —— 旧 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参 confType:1=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(); diff --git a/src/app/panels/columns/ColumnDrawer.cpp b/src/app/panels/columns/ColumnDrawer.cpp index 7d74414..2f3703c 100644 --- a/src/app/panels/columns/ColumnDrawer.cpp +++ b/src/app/panels/columns/ColumnDrawer.cpp @@ -1,34 +1,19 @@ #include "panels/columns/ColumnDrawer.hpp" -#include "panels/columns/Column2DDataset.hpp" #include "panels/columns/CategoryAnalysisTab.hpp" -#include #include "Glyphs.hpp" #include "Theme.hpp" #include #include -#include -#include -#include namespace geopro::app { ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary* dict) : QWidget(parent) { - col2D_ = new Column2DDataset(this); + // 单列承载:去 QTabWidget,body_ 直接为 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(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_; diff --git a/src/app/panels/columns/ColumnDrawer.hpp b/src/app/panels/columns/ColumnDrawer.hpp index 05175c1..65e31cc 100644 --- a/src/app/panels/columns/ColumnDrawer.hpp +++ b/src/app/panels/columns/ColumnDrawer.hpp @@ -2,7 +2,6 @@ #include 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: - // 切换「三维分析 / 二维分析」tab:is2D=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; }; diff --git a/src/controller/DatasetRenderStrategy.cpp b/src/controller/DatasetRenderStrategy.cpp index 628c994..d350943 100644 --- a/src/controller/DatasetRenderStrategy.cpp +++ b/src/controller/DatasetRenderStrategy.cpp @@ -1,15 +1,24 @@ #include "controller/DatasetRenderStrategy.hpp" +#include "controller/VtkSceneController.hpp" // 完整类型 + 含 I3dSceneView(view_.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 diff --git a/src/controller/DatasetRenderStrategy.hpp b/src/controller/DatasetRenderStrategy.hpp index e6cda9d..df28ddd 100644 --- a/src/controller/DatasetRenderStrategy.hpp +++ b/src/controller/DatasetRenderStrategy.hpp @@ -5,6 +5,8 @@ namespace geopro::controller { +class VtkSceneController; // 策略委托回控制器既有渲染路径(add→addDatasetAsync/add2DDatasetAsync;remove→view.removeDataset) + class IDatasetRenderStrategy { public: virtual ~IDatasetRenderStrategy() = default; @@ -27,26 +29,34 @@ private: std::map> strategies_; }; -// 3 骨架策略:本任务仅建类与注册。add/remove 的真实渲染实现由后续阶段填充—— -// Phase B(VolumeRenderStrategy/CurtainRenderStrategy 包现有 3D 分支)与 -// Phase E/F(Plane2DRenderStrategy 平面 z + 底图)。届时各自接所需的 -// VtkSceneController& / View / Repo 引用。当前 .cpp 给空实现使链接通过。 +// 3 策略:各持 VtkSceneController& 引用,把 add/remove 委托回控制器既有渲染路径。 +// Volume/Curtain::add 均转调 addDatasetAsync(其内部按 isVolumeDataset 自分体/帘面分支); +// Plane2D::add 暂转调 add2DDatasetAsync(Phase 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 diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp index a2ec377..31085cf 100644 --- a/src/controller/VtkSceneController.cpp +++ b/src/controller/VtkSceneController.cpp @@ -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(*this)); + registry_.registerStrategy("curtain", std::make_unique(*this)); + registry_.registerStrategy("plane2d", std::make_unique(*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 newDs; - newDs.reserve(static_cast(dsIds.size())); - for (const QString& id : dsIds) newDs.push_back(id.toStdString()); +void VtkSceneController::setCheckedDatasets( + const std::vector>& idType) { + std::map 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 prev = checked_; // diff 快照(区分新增/移除) - // 3D:增量 diff —— 只处理新增/移除,不全量重建(底图、其余 ds、相机均不动)。 - const std::set oldSet(checkedDs_.begin(), checkedDs_.end()); - const std::set 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 newDs; - newDs.reserve(static_cast(dsIds.size())); - for (const QString& id : dsIds) newDs.push_back(id.toStdString()); - - // 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff(不全量重建,不打断 3D 帘面/体)。 - const std::set oldSet(checked2dDs_.begin(), checked2dDs_.end()); - const std::set 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() { // 坏 dsId(loadGrid/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 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。 QPointer 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())); diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp index 2521c0d..44306cb 100644 --- a/src/controller/VtkSceneController.hpp +++ b/src/controller/VtkSceneController.hpp @@ -7,8 +7,11 @@ #include #include #include +#include +#include #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>& idType); + public slots: - void setCheckedDatasets(const QStringList& dsIds); - // 二维数据集栏勾选(足迹型测线/轨迹)→ 平铺进 View3D 地图。与 3D 勾选集独立,按 dsId 增量。 - void setChecked2DDatasets(const QStringList& dsIds); - // 二维足迹摆放高度(mode:0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义;customZ 仅 mode=4 用)。 - void set2DPlacement(int mode, double customZ); void setViewMode(ViewMode mode); // 切「三维分析/二维分析」tab(A 期):按目标维度是否已有数据重置取景基线,使切换后该维度第一条 // 数据自动取景。显隐/相机/坐标轴由 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; // 当前摆放模式下足迹的世界 Z(mode 0=关闭由调用方拦截;此处算 1/2/3/4 的 Z)。 double placementZ() const; @@ -103,13 +113,14 @@ private: data::I3dSceneRepository& sceneRepo_; I3dSceneView& view_; - std::vector checkedDs_; - // 二维足迹勾选集(与 checkedDs_ 独立;都画进 View3D 场景)。按 dsId 增量加/删。 - std::vector checked2dDs_; + // 统一勾选集(2D+3D 合一):dsId→typeId(描述符 id)。增量 diff 的真源;rebuildInternal 据此重放。 + std::map checked_; + // 每 typeId 活跃计数:首勾(0→1)调 onTypeActivated、全消(1→0)调 onTypeDeactivated。 + std::map 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; diff --git a/tests/controller/test_vtk_scene_controller.cpp b/tests/controller/test_vtk_scene_controller.cpp index 9704ae0..1ede691 100644 --- a/tests/controller/test_vtk_scene_controller.cpp +++ b/tests/controller/test_vtk_scene_controller.cpp @@ -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>; +IdType curtainIds(std::initializer_list ids) { + IdType v; + for (const auto& id : ids) v.push_back({id, "resistivity"}); // resistivity → renderStrategyId "curtain" + return v; } +IdType voxelIds(std::initializer_list 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 图元(不整场 clear,3D 增量路径)。 +// 取消勾选 → 增量移除该 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 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 条 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"}); + 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); -}