diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index ef60b00..7efe7d7 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -109,7 +109,6 @@ add_executable(geopro_desktop WIN32 VolumeParamsDialog.cpp VolumePropertiesDialog.cpp Logging.cpp - DatasetDimension.cpp DatasetCategory.cpp VtkViewToolbar.cpp AxesSettingsDialog.cpp diff --git a/src/app/DatasetCategory.hpp b/src/app/DatasetCategory.hpp index 519db3f..df68db3 100644 --- a/src/app/DatasetCategory.hpp +++ b/src/app/DatasetCategory.hpp @@ -1,15 +1,14 @@ #pragma once #include -#include "repo/CategoryConfig.hpp" #include "repo/RepoTypes.hpp" namespace geopro::app { struct CategoryBuckets { - std::vector> segments; // 与 categoryConfigs() 同序同长 + std::vector> segments; // 与 categoryCatalog() 同序同长 }; -// 按 CategoryConfig 把 ds 分入大类段:先判 ddCode 白名单(三维体/切片),否则按 dsTypeCode 匹配; +// 按 categoryCatalog() 把 ds 分入大类段:遍历目录,命中首个 classify(row)==true 的描述符即归入该段; // 不在表内的丢弃(接地电阻/原始数据/白化/坐标等)。保留原顺序。 CategoryBuckets splitByCategory(const std::vector& rows); diff --git a/src/app/DatasetDimension.cpp b/src/app/DatasetDimension.cpp deleted file mode 100644 index b4c1e2d..0000000 --- a/src/app/DatasetDimension.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "DatasetDimension.hpp" - -namespace geopro::app { - -namespace { -// 与 LocalSample3dRepository::dimensionOf 同一映射(spec §6.1)。 -enum class Dim { D3, D2, Analysis, Other }; -Dim dimOf(const std::string& c) { - if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" || - c == "dd_section" || c == "dd_inversion_data" || c == "dd_radar_3d") - return Dim::D3; - if (c == "dd_slice") return Dim::Analysis; - if (c == "dd_trajectory_data") return Dim::D2; - return Dim::Other; -} -} // namespace - -DimBuckets splitByDimension(const std::vector& rows) { - DimBuckets b; - for (const auto& r : rows) { - switch (dimOf(r.ddCode)) { - case Dim::D3: b.dim3D.push_back(r); break; - case Dim::D2: b.dim2D.push_back(r); break; - case Dim::Analysis: b.analysis.push_back(r); break; - case Dim::Other: break; - } - } - return b; -} - -} // namespace geopro::app diff --git a/src/app/DatasetDimension.hpp b/src/app/DatasetDimension.hpp deleted file mode 100644 index 846ce57..0000000 --- a/src/app/DatasetDimension.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include -#include "repo/RepoTypes.hpp" - -namespace geopro::app { - -struct DimBuckets { - std::vector dim3D; - std::vector dim2D; - std::vector analysis; -}; - -// 按 ddCode 把 ds 分流到 三维数据集 / 二维数据集 / 三维分析 三栏。 -// Other 维度不入任何栏(保留原顺序)。 -DimBuckets splitByDimension(const std::vector& rows); - -} // namespace geopro::app diff --git a/src/app/TileBasemap.cpp b/src/app/TileBasemap.cpp index 25ffd33..c907200 100644 --- a/src/app/TileBasemap.cpp +++ b/src/app/TileBasemap.cpp @@ -139,6 +139,10 @@ void TileBasemap::requestRender() { } TileBasemap::~TileBasemap() { + // 移除本实例所有已贴瓦片:多实例(每 2D 平面一份)动态建销时,析构须撤回瓦片,否则渲染器仍持引用、 + // 底图不随平面消失。共享 3D 底图存活至退出故旧码无此清理也无碍,但 per-plane 实例必须清。 + if (auto* ren = scene_.renderer()) + for (auto& kv : placed_) ren->RemoveViewProp(kv.second); if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_); } diff --git a/src/app/TileBasemapPlaneAdapter.hpp b/src/app/TileBasemapPlaneAdapter.hpp new file mode 100644 index 0000000..3756c76 --- /dev/null +++ b/src/app/TileBasemapPlaneAdapter.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include + +#include "TileBasemap.hpp" +#include "controller/DatasetRenderStrategy.hpp" // geopro::controller::IPlaneBasemap + +namespace geopro::render { class Scene; } +namespace geopro::core { class GeoLocalFrame; } +class vtkRenderWindow; + +namespace geopro::app { + +// 2D 平面底图适配器:把 app 层 TileBasemap 适配到控制器层抽象 IPlaneBasemap, +// 使 geopro_controller(仅链 geopro_data+Qt::Core) 不反依赖 app 层与 VTK(仿 I3dSceneView/VtkSceneView)。 +// main.cpp 经底图工厂按平面 z 造之;持 TileBasemap 值成员,随适配器析构而析构(移除瓦片→底图随平面消失)。 +class TileBasemapPlaneAdapter : public geopro::controller::IPlaneBasemap { +public: + TileBasemapPlaneAdapter(geopro::render::Scene& scene, vtkRenderWindow* rw, + std::shared_ptr frame, double groundZ, + std::function radiusProvider) + : bm_(scene, rw, std::move(frame), nullptr, groundZ) { + bm_.setDataRadiusProvider(std::move(radiusProvider)); + } + void show(int kind) override { + bm_.show(kind == 0 ? TileBasemap::Street : TileBasemap::Hidden); + } + void hide() override { bm_.hide(); } + void setOpacity(double o) override { bm_.setOpacity(o); } + +private: + TileBasemap bm_; +}; + +} // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 56c6dc2..ea53bab 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -93,7 +93,6 @@ #include "ApiClient.hpp" #include "AuthService.hpp" -#include "DatasetDimension.hpp" #include "DatasetCategory.hpp" #include "repo/CategoryDescriptor.hpp" #include "Credential.hpp" @@ -143,6 +142,7 @@ #include "panels/DatasetAttrPanel.hpp" #include "panels/ObjectExceptionPanel.hpp" #include "TileBasemap.hpp" +#include "TileBasemapPlaneAdapter.hpp" #include "panels/columns/ColumnDrawer.hpp" #include "panels/columns/CategoryAnalysisTab.hpp" #include "panels/columns/CategorySection.hpp" @@ -1276,6 +1276,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 2D 段「z 值」滑块 → 整体升降该 2D 类型平面(Plane2DRenderStrategy 重摆其全部足迹)。 QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::planeZChanged, sceneCtrl, &geopro::controller::VtkSceneController::setPlaneZ); + // 2D 段「底图」弹窗 → 切该类型平面底图 矢量平面/无 + 透明度(Plane2DRenderStrategy 多实例底图)。 + QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::basemapKindChanged, sceneCtrl, + &geopro::controller::VtkSceneController::setBasemapKind); + QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::basemapOpacityChanged, sceneCtrl, + &geopro::controller::VtkSceneController::setBasemapOpacity); // 反向 VTK→list:在 VTK 里点中/选中一张切片 → 在三维体段树里同步选中该切片行(②反向)。 // 点空白/清选(dsId 空) → 一并清 VTK 异常高亮(否则取消选中后异常图形仍高亮,用户反馈)。 interactionMgr->onSliceSelectionChanged = [drawer, sceneView, renderWindowPtr]( @@ -1322,6 +1327,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re [basemap](double o) { basemap->setOpacity(o); }); // 底图最大范围按当前勾选剖面合并范围动态定(随增删自动伸缩);刷新时实时查询。 basemap->setDataRadiusProvider([sceneView]() { return sceneView->dataHorizontalRadius(); }); + // F2:2D 平面底图工厂下发 → Plane2DRenderStrategy 据此为每个 2D 类型平面按需建/销平面矢量底图 + // (与共享 3D 底图同源 scene/渲染窗/frame/数据半径规则;随平面 z 升降重建、全消随平面消失)。 + // 工厂造 TileBasemapPlaneAdapter(app 层适配 IPlaneBasemap),使 geopro_controller 不反依赖 app/VTK。 + sceneCtrl->setPlaneBasemapFactory( + [scene, renderWindowPtr, frame, sceneView](double groundZ) + -> std::unique_ptr { + return std::make_unique( + *scene, renderWindowPtr, frame, groundZ, + [sceneView]() { return sceneView->dataHorizontalRadius(); }); + }); // 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。单一来源 kVerticalExaggeration 下发 // 控制器(上方)/底图;运行时 VE 改由坐标轴设置抽屉「应用」下发(旧三维数据集栏滑块已退役)。 basemap->setVerticalExaggeration(kVerticalExaggeration); diff --git a/src/app/panels/columns/CategoryAnalysisTab.cpp b/src/app/panels/columns/CategoryAnalysisTab.cpp index 7b8dbe1..515c4e2 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.cpp +++ b/src/app/panels/columns/CategoryAnalysisTab.cpp @@ -69,6 +69,10 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d &CategoryAnalysisTab::anomalyVisibilityChanged); connect(sec, &CategorySection::datasetSelected, this, &CategoryAnalysisTab::datasetSelected); connect(sec, &CategorySection::planeZChanged, this, &CategoryAnalysisTab::planeZChanged); + connect(sec, &CategorySection::basemapKindChanged, this, + &CategoryAnalysisTab::basemapKindChanged); + connect(sec, &CategorySection::basemapOpacityChanged, this, + &CategoryAnalysisTab::basemapOpacityChanged); // #7:各段等分 stretch → 内容都少时四段平分高度填满面板(初始与 VTK 区等高、不出滚动条); // 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。 col->addWidget(sec, 1); diff --git a/src/app/panels/columns/CategoryAnalysisTab.hpp b/src/app/panels/columns/CategoryAnalysisTab.hpp index faa476b..39b548e 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.hpp +++ b/src/app/panels/columns/CategoryAnalysisTab.hpp @@ -53,6 +53,8 @@ signals: void anomalyVisibilityChanged(const QString& dsId, bool vis); void datasetSelected(const QString& dsId, const QString& ddCode); // 树选中→VTK 高亮联动 void planeZChanged(const QString& typeId, double z); // 2D 段 z 值滑块:整体升降该类型平面 + void basemapKindChanged(const QString& typeId, int kind); // 2D 段底图弹窗:矢量平面(0)/无(1) + void basemapOpacityChanged(const QString& typeId, double o); // 2D 段底图弹窗:透明度[0,1] private: void recomputeCheckedUnion(); diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp index d329cdb..708c2f0 100644 --- a/src/app/panels/columns/CategorySection.cpp +++ b/src/app/panels/columns/CategorySection.cpp @@ -88,9 +88,7 @@ CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc, break; case geopro::data::OpKind::Basemap: acts.push_back({QStringLiteral("map"), QStringLiteral("底图"), {}, - [this](QToolButton*) { // Task F2 建底图 popup;当前先发请求信号 - emit basemapPopupRequested(QString::fromStdString(desc_.id)); - }}); + [this](QToolButton* host) { showBasemapPopup(host); }}); break; } } @@ -500,4 +498,54 @@ void CategorySection::showPlaneZPopup(QToolButton* host) { menu.exec(host->mapToGlobal(QPoint(0, host->height()))); } +void CategorySection::showBasemapPopup(QToolButton* host) { + // 底图 popup(仿工具条底图控件):本类型平面底图 矢量平面(默认)/无 + 透明度滑块(0–100,默认 50)。 + // 类型切换为离散单次事件直发;透明度拖动逐步触发→单发 QTimer(150ms)防抖,停手后一次发射,免抖动重铺瓦片。 + QMenu menu(this); + auto* wa = new QWidgetAction(&menu); + auto* box = new QWidget(&menu); + auto* lay = new QVBoxLayout(box); + lay->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kSm); + lay->setSpacing(space::kXs); + + auto* kindCombo = new QComboBox(box); + kindCombo->addItem(QStringLiteral("矢量平面")); // index 0 + kindCombo->addItem(QStringLiteral("无")); // index 1 + kindCombo->setCurrentIndex(lastBasemapKind_); + connect(kindCombo, &QComboBox::currentIndexChanged, this, [this](int idx) { + lastBasemapKind_ = idx; + emit basemapKindChanged(QString::fromStdString(desc_.id), idx); + }); + + auto* lab = new QLabel(box); + auto* sld = new QSlider(Qt::Horizontal, box); + sld->setRange(0, 100); + sld->setValue(lastBasemapOpacity_); + sld->setMinimumWidth(160); + sld->setToolTip(QStringLiteral("底图透明度")); + auto syncLabel = [lab](int v) { lab->setText(QStringLiteral("透明度:%1%").arg(v)); }; + syncLabel(sld->value()); + if (!basemapOpacityTimer_) { // 定时器 parent=this,存活于 modal popup 之外,停手后安全发射终值 + basemapOpacityTimer_ = new QTimer(this); + basemapOpacityTimer_->setSingleShot(true); + basemapOpacityTimer_->setInterval(150); + connect(basemapOpacityTimer_, &QTimer::timeout, this, [this]() { + emit basemapOpacityChanged(QString::fromStdString(desc_.id), pendingBasemapOpacity_); + }); + } + connect(sld, &QSlider::valueChanged, this, [this, syncLabel](int v) { + lastBasemapOpacity_ = v; + syncLabel(v); + pendingBasemapOpacity_ = v / 100.0; + basemapOpacityTimer_->start(); // 重启防抖窗口:覆盖拖动、键盘、点轨——停手后一次性发射 + }); + + lay->addWidget(kindCombo); + lay->addWidget(lab); + lay->addWidget(sld); + wa->setDefaultWidget(box); + menu.addAction(wa); + menu.exec(host->mapToGlobal(QPoint(0, host->height()))); +} + } // namespace geopro::app diff --git a/src/app/panels/columns/CategorySection.hpp b/src/app/panels/columns/CategorySection.hpp index 45e0675..c34da64 100644 --- a/src/app/panels/columns/CategorySection.hpp +++ b/src/app/panels/columns/CategorySection.hpp @@ -54,7 +54,8 @@ signals: void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」(接收方按 sourceIds 解析类型) void planeZChanged(const QString& typeId, double z); // PlaneZ 滑块:整体升降该 2D 类型平面(z=绝对高程,米) - void basemapPopupRequested(const QString& typeId); // Basemap 图标:弹底图选择 popup(Task F2 实现) + void basemapKindChanged(const QString& typeId, int kind); // 底图弹窗:矢量平面(0)/无(1) + void basemapOpacityChanged(const QString& typeId, double o); // 底图弹窗透明度滑块[0,1](防抖发射) void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情 void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常) // ── 三维体段右键操作(迁自旧 Column3DAnalysis,全接)── @@ -71,6 +72,7 @@ signals: private: void showContextMenu(const QPoint& pos); // 段体树右键菜单(详情 + 删除) void showPlaneZPopup(QToolButton* host); // PlaneZ 图标:弹 z 值滑块 popup → planeZChanged + void showBasemapPopup(QToolButton* host); // Basemap 图标:弹 矢量平面/无 + 透明度滑块 popup void rebuildList(); // 据 rows_(经装置/日期筛选)重建段体树并复原勾选 void refreshArrayCombo(); // 据当前 rows_ 重填装置类型下拉项(经字典 value→中文) void emitChecked(); // 收集勾选 → checkedDatasetsChanged @@ -94,6 +96,10 @@ private: double lastPlaneZ_ = 0.0; // 上次 z 值滑块设定的平面高程(重开 popup 时回显,无则 0) QTimer* planeZTimer_ = nullptr; // z 值滑块发射防抖:拖动/键盘/点轨期合并为停手后一次重摆(owned by this,存活于 popup 之外) double pendingPlaneZ_ = 0.0; // 防抖待发的平面 z(定时器到点时取此值发射 planeZChanged) + int lastBasemapKind_ = 0; // 上次底图选择(0=矢量平面/1=无),重开 popup 时回显 + int lastBasemapOpacity_ = 50; // 上次底图透明度(0–100,重开 popup 回显;默认 50) + QTimer* basemapOpacityTimer_ = nullptr; // 底图透明度滑块发射防抖(同 planeZ:停手后一次发射,免抖动重铺瓦片) + double pendingBasemapOpacity_ = 0.5; // 防抖待发的底图透明度[0,1](定时器到点发射 basemapOpacityChanged) }; } // namespace geopro::app diff --git a/src/controller/DatasetRenderStrategy.cpp b/src/controller/DatasetRenderStrategy.cpp index f04cd37..adcd044 100644 --- a/src/controller/DatasetRenderStrategy.cpp +++ b/src/controller/DatasetRenderStrategy.cpp @@ -4,6 +4,10 @@ namespace geopro::controller { +namespace { +constexpr double kDefaultBasemapOpacity = 0.5; // 平面底图默认半透明(地下足迹可透过看) +} + // 各策略委托回控制器既有渲染路径(友元访问其私有 addDatasetAsync/add2DDatasetAsync/view_)。 // 增量 gen 沿用控制器当前 rebuildGeneration_(不自增,与并发增量互不作废)。 void VolumeRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) { @@ -30,8 +34,21 @@ void Plane2DRenderStrategy::remove(const std::string& dsId) { planeReg_.onUnchecked(it->second, dsId); // 该类型成员集空时平面自动消失 dsToType_.erase(it); } -// 该类型全消(控制器活跃计数归零):此时 planeReg_.hasPlane==false。Phase F2 在此销毁平面底图。 -void Plane2DRenderStrategy::onTypeDeactivated(const std::string& /*typeId*/) {} +// 该类型首勾(控制器活跃计数 0→1,先于 add 触发):建该类型平面矢量底图。 +// 注意 onTypeActivated 在 add 之前触发 → 此刻 planeReg_ 尚无该平面,createBasemap 以 zRefElev() 兜底 +// (即首勾 add 即将写入的平面 z,二者一致)。kind/opacity 复位为默认(矢量平面/0.5)。 +void Plane2DRenderStrategy::onTypeActivated(const std::string& typeId) { + bmKind_[typeId] = 0; + bmOpacity_[typeId] = kDefaultBasemapOpacity; + createBasemap(typeId); +} + +// 该类型全消(控制器活跃计数 1→0):销毁该类型底图(析构移除瓦片→底图随平面一并消失),遗忘其 kind/opacity。 +void Plane2DRenderStrategy::onTypeDeactivated(const std::string& typeId) { + bms_.erase(typeId); + bmKind_.erase(typeId); + bmOpacity_.erase(typeId); +} void Plane2DRenderStrategy::setPlaneZ(const std::string& typeId, double z) { planeReg_.setPlaneZ(typeId, z); // 平面 z 真源更新(类型不存在则无操作) @@ -42,7 +59,38 @@ void Plane2DRenderStrategy::setPlaneZ(const std::string& typeId, double z) { ctrl_.view_.removeDataset(dsId); ctrl_.add2DDatasetAsync(dsId, ctrl_.rebuildGeneration_, z); } + // 底图同步升降:销毁旧实例、按新平面 z 重建(复用当前 kind/opacity),简单可靠(无 setGroundZ 增量重铺)。 + if (bms_.count(typeId)) createBasemap(typeId); ctrl_.view_.renderIncremental(); } +void Plane2DRenderStrategy::setBasemapKind(const std::string& typeId, int kind) { + bmKind_[typeId] = kind; + auto it = bms_.find(typeId); + if (it == bms_.end()) return; + if (kind == 0) it->second->show(0); // 矢量平面 + else it->second->hide(); // 无(保留实例,仅隐瓦片) +} + +void Plane2DRenderStrategy::setBasemapOpacity(const std::string& typeId, double o) { + bmOpacity_[typeId] = o; + auto it = bms_.find(typeId); + if (it != bms_.end()) it->second->setOpacity(o); +} + +void Plane2DRenderStrategy::createBasemap(const std::string& typeId) { + if (!basemapFactory_) return; // 工厂未注入(如纯逻辑单测,无 VTK) → 不建底图 + // 平面 z:已建平面取 planeReg_(setPlaneZ 重建走此);onTypeActivated 时平面尚未建 → 以 zRefElev() 兜底 + // (= add 即将写入的首勾平面 z,二者一致)。 + const double gz = + planeReg_.hasPlane(typeId) ? planeReg_.planeZ(typeId) : ctrl_.view_.zRefElev(); + auto bm = basemapFactory_(gz); + if (!bm) return; + const int kind = bmKind_.count(typeId) ? bmKind_[typeId] : 0; + const double op = bmOpacity_.count(typeId) ? bmOpacity_[typeId] : kDefaultBasemapOpacity; + bm->setOpacity(op); // 先定透明度,再 show 套用到初次铺瓦 + if (kind == 0) bm->show(0); // 默认矢量平面;kind=1(无)则仅留实例 + bms_[typeId] = std::move(bm); // 替换旧实例 → 旧底图适配器析构移除其瓦片 +} + } // namespace geopro::controller diff --git a/src/controller/DatasetRenderStrategy.hpp b/src/controller/DatasetRenderStrategy.hpp index 17db96f..f11532c 100644 --- a/src/controller/DatasetRenderStrategy.hpp +++ b/src/controller/DatasetRenderStrategy.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -9,6 +10,20 @@ namespace geopro::controller { class VtkSceneController; // 策略委托回控制器既有渲染路径(add→addDatasetAsync/add2DDatasetAsync;remove→view.removeDataset) +// 平面底图抽象:控制器层不依赖 VTK/app(geopro_controller 仅链 geopro_data+Qt::Core)。 +// app 层(main.cpp)经工厂注入具体 TileBasemap 适配器,仿既有 I3dSceneView/VtkSceneView 边界, +// 避免 geopro_controller 反向依赖 app 层与 VTK。 +class IPlaneBasemap { +public: + virtual ~IPlaneBasemap() = default; + virtual void show(int kind) = 0; // 0=矢量平面(Street)/其它=无(hide) + virtual void hide() = 0; + virtual void setOpacity(double o) = 0; // 半透明度[0,1] +}; +// 工厂:按平面 z 造一份平面底图(底图所需 scene/渲染窗/frame/数据半径规则由 app 闭包捕获)。 +// 未注入(空)则不建底图——便于无 VTK 的纯逻辑单测。 +using PlaneBasemapFactory = std::function(double groundZ)>; + class IDatasetRenderStrategy { public: virtual ~IDatasetRenderStrategy() = default; @@ -59,13 +74,26 @@ 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; - void onTypeDeactivated(const std::string& typeId) override; - // 滑块整体升降该类型平面 z:更新 planeReg_ 后,对该类型已勾选足迹移除并按新 z 重摆。 + void onTypeActivated(const std::string& typeId) override; // 首勾:建该类型平面矢量底图 + void onTypeDeactivated(const std::string& typeId) override; // 全消:销毁该类型底图(瓦片随之消失) + // 滑块整体升降该类型平面 z:更新 planeReg_ 后,对该类型已勾选足迹移除并按新 z 重摆 + 底图重建于新 z。 void setPlaneZ(const std::string& typeId, double z); + // 底图工厂注入(main.cpp 构造后一次性下发;未注入则底图建造静默跳过,便于纯逻辑单测)。 + void setBasemapFactory(PlaneBasemapFactory f) { basemapFactory_ = std::move(f); } + void setBasemapKind(const std::string& typeId, int kind); // 0=矢量平面(show)/1=无(hide) + void setBasemapOpacity(const std::string& typeId, double o); // 该类型底图半透明度[0,1] private: + void createBasemap(const std::string& typeId); // 按当前 z/kind/opacity 建(或重建)该类型底图 + VtkSceneController& ctrl_; PlaneZRegistry planeReg_; // 按类型的平面 z 生命周期 std::map dsToType_; // dsId→typeId(remove 只得 dsId,需自存反查) + + // 每 2D 类型一份平面矢量底图(贴该类型平面 z);随平面建/销/升降。键=typeId。 + std::map> bms_; + std::map bmKind_; // 该类型底图选择(0=矢量平面/1=无),重建时复用 + std::map bmOpacity_; // 该类型底图透明度,重建时复用 + PlaneBasemapFactory basemapFactory_; // app 注入的底图工厂(空=不建底图) }; } // namespace geopro::controller diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp index 12b1bd0..3ec168a 100644 --- a/src/controller/VtkSceneController.cpp +++ b/src/controller/VtkSceneController.cpp @@ -32,6 +32,18 @@ void VtkSceneController::setPlaneZ(const QString& typeId, double z) { if (plane2d_) plane2d_->setPlaneZ(typeId.toStdString(), z); } +void VtkSceneController::setPlaneBasemapFactory(PlaneBasemapFactory factory) { + if (plane2d_) plane2d_->setBasemapFactory(std::move(factory)); +} + +void VtkSceneController::setBasemapKind(const QString& typeId, int kind) { + if (plane2d_) plane2d_->setBasemapKind(typeId.toStdString(), kind); +} + +void VtkSceneController::setBasemapOpacity(const QString& typeId, double opacity) { + if (plane2d_) plane2d_->setBasemapOpacity(typeId.toStdString(), opacity); +} + IDatasetRenderStrategy* VtkSceneController::strategyForType(const std::string& typeId) const { for (const auto& d : geopro::data::categoryCatalog()) if (d.id == typeId) return registry_.get(d.renderStrategyId); @@ -316,12 +328,6 @@ void VtkSceneController::zoomIn() { view_.zoom(1.2); } void VtkSceneController::zoomOut() { view_.zoom(1.0 / 1.2); } void VtkSceneController::fit() { view_.fitView(); } -const geopro::core::Grid& VtkSceneController::grid(const std::string& dsId) { - auto it = gridCache_.find(dsId); - if (it == gridCache_.end()) it = gridCache_.emplace(dsId, dsRepo_.loadGrid(dsId)).first; - return it->second; -} - const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string& dsId) { auto it = colorScaleCache_.find(dsId); if (it == colorScaleCache_.end()) @@ -331,7 +337,6 @@ const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string void VtkSceneController::rebuildInternal() { const unsigned long long gen = ++rebuildGeneration_; // 自增:作废此前所有在途增量回调 - const bool is2D = (mode_ == ViewMode::Map2D); view_.clear(); // 移除全部数据图元(保留底图);frame 重锚标志复位 loadingDs_.clear(); // 旧在途加载随之作废(回调按 gen 丢弃) @@ -343,34 +348,30 @@ void VtkSceneController::rebuildInternal() { // 坏 dsId(loadGrid/loadColorScale 抛异常)= best-effort 跳过:emit loadFailed 但不中断。 try { - if (is2D) { - for (const auto& [dsId, typeId] : checked_) view_.addSurveyLine(grid(dsId)); - } else { - // 回调用 QPointer 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。 - QPointer self(this); - if (showTerrain_) { - sceneRepo_.loadTerrainPaths( - [self, gen](data::TerrainPaths p) { - if (!self || gen != self->rebuildGeneration_) return; // 已析构/迟到:丢弃 - self->view_.addTerrain(std::move(p)); - self->onDatasetArrived(); - }, - [self, gen](const std::string& m) { - if (!self || gen != self->rebuildGeneration_) return; - emit self->loadFailed(QString::fromStdString(m)); - }); - } - // 全量重建:clear 已移除全部图元,据统一勾选集经各 ds 类型策略重放 add(不动活跃计数: - // 已在 setCheckedDatasets 计入;策略 add 内部转调 addDatasetAsync/add2DDatasetAsync)。 - for (const auto& [dsId, typeId] : checked_) - if (auto* s = strategyForType(typeId)) s->add(typeId, dsId); + // 回调用 QPointer 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。 + QPointer self(this); + if (showTerrain_) { + sceneRepo_.loadTerrainPaths( + [self, gen](data::TerrainPaths p) { + if (!self || gen != self->rebuildGeneration_) return; // 已析构/迟到:丢弃 + self->view_.addTerrain(std::move(p)); + self->onDatasetArrived(); + }, + [self, gen](const std::string& m) { + if (!self || gen != self->rebuildGeneration_) return; + emit self->loadFailed(QString::fromStdString(m)); + }); } + // 全量重建: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())); } - // 保留相机重建(改VE):不 ResetCamera,原地按新夸张重绘。 - view_.render(is2D, /*resetCamera=*/!preserveCameraOnRebuild_); + // 保留相机重建(改VE):不 ResetCamera,原地按新夸张重绘。视图恒三维(is2D=false)。 + view_.render(/*is2D=*/false, /*resetCamera=*/!preserveCameraOnRebuild_); } } // namespace geopro::controller diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp index e5a2d37..ac5c354 100644 --- a/src/controller/VtkSceneController.hpp +++ b/src/controller/VtkSceneController.hpp @@ -24,8 +24,8 @@ namespace geopro::controller { class DatasetViewState; // 跨视图共享色阶真源(统一同步机制) -// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。 -enum class ViewMode { Map2D, View3D }; +// 中央视图模式:固定三维视图(帘面/体素/地形)。旧二维俯视测线(Map2D)路径已退役(main 恒 View3D)。 +enum class ViewMode { View3D }; // 三维图层("视图详情"浮层勾选)。 enum class SceneLayer { Curtain, Voxel, Terrain }; @@ -49,6 +49,10 @@ public: // 构造后由 main.cpp 注入一次。 void setViewState(DatasetViewState* state); + // 注入 2D 平面底图工厂(app 层闭包捕获 scene/渲染窗/frame/数据半径规则,造 TileBasemap 适配器): + // 转交 Plane2DRenderStrategy,供各类型平面按需建底图。main.cpp 构造后一次性下发。 + void setPlaneBasemapFactory(PlaneBasemapFactory factory); + public: // 勾选并集统一入口(取代旧 setCheckedDatasets(QStringList)/setChecked2DDatasets): // 每项 = (dsId, typeId=描述符 id)。diff vs 上次后按 catalog[typeId].renderStrategyId 派给策略 @@ -78,6 +82,9 @@ public slots: const AxisRangeCfg& z); // 2D 段「z 值」滑块:整体升降某 2D 类型平面(含其上全部已勾选足迹)。转交 Plane2DRenderStrategy。 void setPlaneZ(const QString& typeId, double z); + // 2D 段「底图」弹窗:切该类型平面底图 矢量平面(0)/无(1) + 透明度[0,1]。转交 Plane2DRenderStrategy。 + void setBasemapKind(const QString& typeId, int kind); + void setBasemapOpacity(const QString& typeId, double opacity); void applyView(ViewDir dir); // 6 向快捷视图 void zoomIn(); // Zoom In (×1.2) @@ -119,7 +126,7 @@ private: // 渲染策略注册表(构造时注册 volume/curtain/plane2d 三策略,各持本控制器引用)。 RenderStrategyRegistry registry_; Plane2DRenderStrategy* plane2d_ = nullptr; // registry_ 中 plane2d 策略的裸指针(setPlaneZ 免下转型) - ViewMode mode_ = ViewMode::Map2D; + ViewMode mode_ = ViewMode::View3D; bool showCurtain_ = true; bool showVoxel_ = false; bool showTerrain_ = false; @@ -135,7 +142,6 @@ private: QPointer state_; // 跨视图色阶真源(注入;编辑/加载/重着色都经它;QPointer 防悬挂) // 缓存(按 dsId):避免重复读盘/插值。 - std::map gridCache_; std::map colorScaleCache_; // 帘面源网格缓存:帘面重着色需 grid 重建 addCurtain(loadSection 的 s.grid 不在 gridCache_)。 std::map sectionGridCache_; @@ -154,7 +160,6 @@ private: // 正在加载的 ds:防重复勾选竞态重复请求;全量重建时清空。 std::set loadingDs_; - const geopro::core::Grid& grid(const std::string& dsId); const geopro::core::ColorScale& colorScale(const std::string& dsId); }; diff --git a/src/data/repo/CategoryConfig.hpp b/src/data/repo/CategoryConfig.hpp deleted file mode 100644 index f6c4b0f..0000000 --- a/src/data/repo/CategoryConfig.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include -#include - -namespace geopro::app { - -// 一个数据类型大类段的配置(spec §5)。识别键二选一:dsTypeCode 优先;ddCode 用于三维体/切片。 -struct CategorySpec { - std::string id; // 段稳定 id - std::string title; // 段标题(UI 显示) - std::string dsTypeCode; // 主识别键(空=不按 dsTypeCode) - std::string ddCode; // 次识别键(dd_voxel/dd_slice;空=不按 ddCode) - bool canGenerateVolume; // 段内是否提供「生成三维体」入口(仅反演类) - bool hasArrayTypeFilter; // 段头是否显示装置类型筛选(仅 ERT 类) -}; - -// 5 段固定有序(spec §5 表)。 -inline const std::vector& categoryConfigs() { - static const std::vector kCfg = { - {"resistivity", "电阻率数据", "ERT platform inversion data", "", true, true}, - {"apparent", "视电阻率数据", "visual resistivity data", "", true, true}, - {"transient", "瞬变电磁数据", "DD TRANSIENT ELECTROMAGNETIC INVERSION", "", true, false}, - {"voxel", "三维体", "", "dd_voxel", false, false}, - // 切片不单列段——挂在三维体段「体→切片/异常」三级树下(spec §8 修订)。 - }; - return kCfg; -} - -} // namespace geopro::app diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bce98e3..e9559c1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -172,11 +172,6 @@ target_sources(geopro_tests PRIVATE app/test_color_scale_io.cpp ${CMAKE_SOURCE_DIR}/src/app/ColorScaleIO.cpp ) -# 维度过滤纯函数(splitByDimension: ddCode -> 三维/二维/分析三栏,无 Qt/VTK 依赖)。 -target_sources(geopro_tests PRIVATE - app/test_dataset_dimension.cpp - ${CMAKE_SOURCE_DIR}/src/app/DatasetDimension.cpp -) # 大类分类纯函数(splitByCategory: dsTypeCode/ddCode -> 5 个数据类型大类段,无 Qt/VTK 依赖)。 target_sources(geopro_tests PRIVATE app/test_dataset_category.cpp diff --git a/tests/app/test_dataset_dimension.cpp b/tests/app/test_dataset_dimension.cpp deleted file mode 100644 index 6d5852d..0000000 --- a/tests/app/test_dataset_dimension.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include "DatasetDimension.hpp" -#include "repo/RepoTypes.hpp" - -using geopro::data::DsRow; -using geopro::app::splitByDimension; -using geopro::app::DimBuckets; - -static DsRow row(const char* id, const char* ddCode) { - DsRow r; r.id = id; r.ddCode = ddCode; return r; -} - -TEST(DatasetDimension, SplitsByDdCode) { - std::vector in{ - row("a", "dd_section"), // 3D - row("b", "dd_voxel"), // 3D - row("f", "dd_radar_3d"), // 3D(三维雷达体,spec §6.1) - row("c", "dd_trajectory_data"), // 2D - row("d", "dd_slice"), // Analysis - row("e", "dd_unknownxyz"), // Other -> not in any bucket - }; - DimBuckets b = splitByDimension(in); - ASSERT_EQ(b.dim3D.size(), 3u); - EXPECT_EQ(b.dim3D[0].id, "a"); - EXPECT_EQ(b.dim3D[1].id, "b"); - EXPECT_EQ(b.dim3D[2].id, "f"); - ASSERT_EQ(b.dim2D.size(), 1u); - EXPECT_EQ(b.dim2D[0].id, "c"); - ASSERT_EQ(b.analysis.size(), 1u); - EXPECT_EQ(b.analysis[0].id, "d"); -} - -TEST(DatasetDimension, EmptyInput) { - DimBuckets b = splitByDimension({}); - EXPECT_TRUE(b.dim3D.empty()); - EXPECT_TRUE(b.dim2D.empty()); - EXPECT_TRUE(b.analysis.empty()); -}