docs(vtk): 合并数据集单栏重构 实现计划(6阶段13任务)
A 段配置加维度+trajectory分流;B 单列骨架(动态显隐+空占位+去tab); C 响应式图标条+筛选折叠;D 导入雷达移设备菜单+3D底图移渲染工具栏; E 单一自由场景+2D按类型平面z;F 2D平面底图(TileBasemap多实例)。 纯逻辑TDD(段分流/图标溢出/平面z生命周期),UI走build.bat+手动验收。
This commit is contained in:
parent
ac50aaa51e
commit
dba1b32a43
|
|
@ -0,0 +1,841 @@
|
||||||
|
# VTK 合并数据集单栏重构 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 把「三维分析 + 二维分析」两 tab 合并为一个无标题单列数据集栏,2D/3D 同列分段、按数据动态显隐、段操作改响应式图标工具条,2D 数据以「按类型一块平面 + 平面底图」与 3D 体/帘面在同一自由透视场景共存。
|
||||||
|
|
||||||
|
**Architecture:** 复用方案 A —— 将 `CategoryAnalysisTab` 升级为统一单列 `DatasetColumn`,用扩展的段配置(增 `dimension`)同时驱动 3D/2D 段;新增响应式 `SectionIconBar`;删 `Column2DDataset`/`QTabWidget`;2D 渲染改「按类型平面 z」并由 `TileBasemap` 多实例提供 N 个平面底图;3D 底图控件移渲染区工具栏、导入雷达移顶部设备菜单。
|
||||||
|
|
||||||
|
**Tech Stack:** C++17、Qt6(Widgets)、VTK 9.6.2、GoogleTest/CTest、CMake(经 `build.bat`)。
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-06-30-vtk-merged-dataset-column-refactor-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- 构建:`build.bat app`(编 app)/ `build.bat test`(编+跑测试)。**cmake 不在 PATH**,用 VS 自带;**用户须关闭运行中的 app 才能重链**(LNK1104 是锁,提示用户关闭,不要自行重试绕过)。
|
||||||
|
- 提交:按具体文件 `git add`(**绝不 `-A`**);**禁用署名**(全局规则,无 Co-Authored-By);中文 conventional commits(`feat`/`fix`/`refactor`/`docs`/`test`),无尾随空格。
|
||||||
|
- `.bat`/脚本必须纯 ASCII。
|
||||||
|
- 命名:类型 `PascalCase`、成员 `snake_case_` 尾下划线、常量 `kPascalCase`、命名空间 `geopro::app` 等(随现有文件)。
|
||||||
|
- CJK 路径坑:`std::ifstream` 不吃中文路径(本计划不涉文件读,注意即可)。
|
||||||
|
- 不可变/小文件/函数 <50 行/文件 <800 行原则;surgical changes,仅改与需求直接相关处。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构(创建/修改一览)
|
||||||
|
|
||||||
|
**新建**
|
||||||
|
- `src/app/panels/columns/SectionIconBar.hpp/.cpp` — 段头响应式图标工具条(默认≤3,超出/挤压收进「…」)。
|
||||||
|
- `src/controller/PlaneBasemapManager.hpp/.cpp` — 按 2D 类型管理「平面 z + 平面底图实例」生命周期(首勾建/全消销毁)。
|
||||||
|
- `tests/app/test_section_icon_bar.cpp` — 图标溢出可见数纯逻辑测试。
|
||||||
|
- `tests/controller/test_plane_basemap_manager.cpp` — 平面 z 生命周期纯逻辑测试。
|
||||||
|
|
||||||
|
**修改**
|
||||||
|
- `src/app/DatasetCategory.hpp`、`src/data/repo/CategoryConfig.hpp`、`src/app/DatasetCategory.cpp` — 段配置加 `dimension` + trajectory 段 + 分流。
|
||||||
|
- `src/app/panels/columns/CategoryAnalysisTab.hpp/.cpp` → 升级为 `DatasetColumn`:动态显隐 + 空占位 + 段维度分发。
|
||||||
|
- `src/app/panels/columns/CategorySection.hpp/.cpp` — 段头接 `SectionIconBar`;筛选折叠;新增三维体图标化;2D 段 z值/底图图标;移除导入雷达。
|
||||||
|
- `src/app/panels/columns/ColumnDrawer.hpp/.cpp` — 去 tab + 去 `Column2DDataset`,单列承载。
|
||||||
|
- `src/app/TopBar.hpp/.cpp`(及 `buildDeviceMenu` 所在 TU)— 设备菜单加导入雷达。
|
||||||
|
- `src/app/VtkViewToolbar.hpp/.cpp` — Gear 下加「地图」按钮 + popup;去 `setAnalysisMode2D` 6 向禁用。
|
||||||
|
- `src/app/TileBasemap.hpp/.cpp` — `groundZ`/`opacity` 参数化、Street 纯平、多实例可用。
|
||||||
|
- `src/controller/VtkSceneController.hpp/.cpp` — 接 `PlaneBasemapManager`;2D 落点改「按类型平面 z」;移除 `view2DMode`/`setAnalysisMode2D` 耦合。
|
||||||
|
- `src/app/VtkSceneView.hpp/.cpp` — 移除 `setAnalysisMode2D` 显隐/相机耦合、逐 ds 拖 Z(`nudgeSelectedMapLinesZ`/`mapLineZOffset_`)。
|
||||||
|
- `src/app/main.cpp` — 接线改造(单列喂 2D 数据、导入雷达改接 TopBar、3D 底图改接工具栏、删 col2D/analysisMode 链)。
|
||||||
|
- `tests/app/test_dataset_category.cpp`、`tests/CMakeLists.txt` — 测试扩展/登记。
|
||||||
|
|
||||||
|
**删除(评估后)**
|
||||||
|
- `src/app/panels/columns/Column2DDataset.hpp/.cpp` — 合并后退役(Phase B 末确认无引用再删)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 阶段总览
|
||||||
|
|
||||||
|
- **Phase A**:段配置加维度 + trajectory 段分流(纯逻辑 TDD)。
|
||||||
|
- **Phase B**:单列面板骨架(动态显隐 + 空占位 + 去 tab)。构建后 2D/3D 同列可见。
|
||||||
|
- **Phase C**:响应式图标工具条 + 筛选折叠 + 新增三维体图标化。
|
||||||
|
- **Phase D**:导入雷达移顶部设备菜单 + 3D 底图移渲染工具栏(含 `TileBasemap` 透明度参数化)。
|
||||||
|
- **Phase E**:单一自由场景 + 2D 按类型平面 z(纯逻辑 TDD + 接线)。
|
||||||
|
- **Phase F**:2D 平面底图(`TileBasemap` 多实例 + 管理器 + 段底图 popup)。
|
||||||
|
|
||||||
|
每个 Phase 结束都应 `build.bat test` 绿 + `build.bat app` 可运行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase A:段配置 + 维度分流
|
||||||
|
|
||||||
|
### Task A1: CategorySpec 加 dimension + trajectory 段 + splitByCategory 路由
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/data/repo/CategoryConfig.hpp`
|
||||||
|
- Modify: `src/app/DatasetCategory.cpp:5-20`
|
||||||
|
- Test: `tests/app/test_dataset_category.cpp`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `enum class CategoryDim { D3, D2 };`;`CategorySpec` 增成员 `CategoryDim dim;`;`categoryConfigs()` 末尾新增 id=`"trajectory"` 段(`ddCode="dd_trajectory_data"`, `dim=D2`);`splitByCategory` 对 `dd_trajectory_data` 命中 trajectory 段。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试** — 在 `tests/app/test_dataset_category.cpp` 末尾追加:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
TEST(DatasetCategory, RoutesTrajectoryRowToTrajectorySegment) {
|
||||||
|
using geopro::data::DsRow;
|
||||||
|
DsRow traj;
|
||||||
|
traj.id = "t1";
|
||||||
|
traj.ddCode = "dd_trajectory_data";
|
||||||
|
auto b = geopro::app::splitByCategory({traj});
|
||||||
|
const auto& cfg = geopro::app::categoryConfigs();
|
||||||
|
int idx = -1;
|
||||||
|
for (std::size_t i = 0; i < cfg.size(); ++i)
|
||||||
|
if (cfg[i].id == "trajectory") idx = static_cast<int>(i);
|
||||||
|
ASSERT_GE(idx, 0) << "categoryConfigs must contain a 'trajectory' segment";
|
||||||
|
EXPECT_EQ(cfg[idx].dim, geopro::app::CategoryDim::D2);
|
||||||
|
ASSERT_EQ(b.segments[idx].size(), 1u);
|
||||||
|
EXPECT_EQ(b.segments[idx][0].id, "t1");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetCategory, InversionSegmentsAreD3) {
|
||||||
|
const auto& cfg = geopro::app::categoryConfigs();
|
||||||
|
for (const auto& c : cfg)
|
||||||
|
if (c.id == "resistivity" || c.id == "voxel")
|
||||||
|
EXPECT_EQ(c.dim, geopro::app::CategoryDim::D3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测试确认失败** — `build.bat test`;预期 `test_dataset_category` 编译失败(`CategoryDim` 未定义 / `dim` 成员不存在)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 改 `CategoryConfig.hpp`** — 在 `namespace geopro::app {` 内 `struct CategorySpec` 上方加枚举,并给 struct 加成员、给 4 个现有段补 `dim`、追加 trajectory 段:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class CategoryDim { D3, D2 }; // 段维度:三维(体/帘面) / 二维(轨迹/平面)
|
||||||
|
|
||||||
|
struct CategorySpec {
|
||||||
|
std::string id;
|
||||||
|
std::string title;
|
||||||
|
std::string dsTypeCode;
|
||||||
|
std::string ddCode;
|
||||||
|
bool canGenerateVolume;
|
||||||
|
bool hasArrayTypeFilter;
|
||||||
|
CategoryDim dim; // 维度(图标集/渲染路由用)
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const std::vector<CategorySpec>& categoryConfigs() {
|
||||||
|
static const std::vector<CategorySpec> kCfg = {
|
||||||
|
{"resistivity", "电阻率数据", "ERT platform inversion data", "", true, true, CategoryDim::D3},
|
||||||
|
{"apparent", "视电阻率数据", "visual resistivity data", "", true, true, CategoryDim::D3},
|
||||||
|
{"transient", "瞬变电磁数据", "DD TRANSIENT ELECTROMAGNETIC INVERSION", "", true, false, CategoryDim::D3},
|
||||||
|
{"voxel", "三维体", "", "dd_voxel", false, false, CategoryDim::D3},
|
||||||
|
{"trajectory", "轨迹数据", "", "dd_trajectory_data", false, false, CategoryDim::D2},
|
||||||
|
};
|
||||||
|
return kCfg;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: `splitByCategory` 已按 ddCode 匹配**(`DatasetCategory.cpp:12-13` 现逻辑已覆盖 `dd_trajectory_data`,因 trajectory 段有 ddCode)。**无需改 `DatasetCategory.cpp` 函数体**,仅确认编译。若 `DsRow` 无 `id` 字段以外用到的成员,按实际字段名调整测试。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 跑测试确认通过** — `build.bat test`;预期 `DatasetCategory.*` 全 PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/data/repo/CategoryConfig.hpp tests/app/test_dataset_category.cpp
|
||||||
|
git commit -m "feat(vtk): 段配置加维度 + trajectory 2D 段并入 splitByCategory"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B:单列面板骨架(动态显隐 + 空占位 + 去 tab)
|
||||||
|
|
||||||
|
### Task B1: DatasetColumn 动态显隐空段 + 空面板占位
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/panels/columns/CategoryAnalysisTab.hpp`
|
||||||
|
- Modify: `src/app/panels/columns/CategoryAnalysisTab.cpp:88-97`(`setBuckets`)、构造函数(加占位)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `categoryConfigs()`(含 trajectory 段)。
|
||||||
|
- Produces: 段在其 bucket 为空时 `hide()`、非空 `show()`;全空时显示占位 `QLabel`。`setBuckets` 现在也分发 trajectory 段(2D)。
|
||||||
|
|
||||||
|
> 说明:保持类名 `CategoryAnalysisTab` 不变(避免大范围改名风险),仅扩展职责为「统一单列」。Phase B 末在文档注释标注其新职责。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 构造函数加占位 label** — 在 `CategoryAnalysisTab.cpp` 构造函数 `scroll->setWidget(content);` 之前,于 `content` 布局内(`col` 之外、`outer` 层)添加居中占位。改为在 `outer` 上叠一个占位 label,并存指针:
|
||||||
|
|
||||||
|
`CategoryAnalysisTab.hpp` private 区加:
|
||||||
|
```cpp
|
||||||
|
QWidget* placeholder_ = nullptr; // 全空时显示的占位提示
|
||||||
|
void updatePlaceholderAndVisibility(); // 据各段数据有无 显隐段 + 占位
|
||||||
|
```
|
||||||
|
|
||||||
|
`CategoryAnalysisTab.cpp` 构造函数末尾(`scroll->setWidget(content);` 之后):
|
||||||
|
```cpp
|
||||||
|
placeholder_ = new QLabel(QStringLiteral("请在左侧对象树勾选测线 / 数据集"), this);
|
||||||
|
placeholder_->setAlignment(Qt::AlignCenter);
|
||||||
|
placeholder_->setWordWrap(true);
|
||||||
|
applyTokenizedStyleSheet(placeholder_,
|
||||||
|
QStringLiteral("QLabel{color:{{text/tertiary}};padding:24px;}"));
|
||||||
|
outer->addWidget(placeholder_, 0); // 与 scroll 同级;由 updatePlaceholderAndVisibility 切显隐
|
||||||
|
placeholder_->hide();
|
||||||
|
```
|
||||||
|
(顶部 include 补 `#include <QLabel>`。)
|
||||||
|
|
||||||
|
- [ ] **Step 2: 实现 `updatePlaceholderAndVisibility`** — 段有无数据按其 list 行数判定(容器节点不算)。在 `CategoryAnalysisTab.cpp` 加:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void CategoryAnalysisTab::updatePlaceholderAndVisibility() {
|
||||||
|
bool anyVisible = false;
|
||||||
|
for (auto* sec : ordered_) {
|
||||||
|
const bool has = sec->hasRenderableRows(); // Task B1-Step3 在 CategorySection 新增
|
||||||
|
sec->setVisible(has);
|
||||||
|
if (has) anyVisible = true;
|
||||||
|
}
|
||||||
|
if (scroll_) scroll_->setVisible(anyVisible);
|
||||||
|
if (placeholder_) placeholder_->setVisible(!anyVisible);
|
||||||
|
relayoutSections();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: CategorySection 加 `hasRenderableRows()`** — `CategorySection.hpp` public 加 `bool hasRenderableRows() const;`;`.cpp` 实现(数据行 = 非 container 的可勾选行存在):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool CategorySection::hasRenderableRows() const {
|
||||||
|
if (!list_) return false;
|
||||||
|
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||||||
|
if ((*it)->data(0, kDsDdCodeRole).toString() != QStringLiteral("container"))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: `setBuckets` 分发 trajectory + 触发显隐** — 改 `CategoryAnalysisTab.cpp:88-97`:去掉对 voxel 的 `continue` 之外保持,循环末尾调用显隐刷新。注意 trajectory 段(D2)数据来自 splitByCategory 第 5 桶,自然分发;voxel 仍跳过(mock 注入):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
|
||||||
|
const auto& cfg = categoryConfigs();
|
||||||
|
for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) {
|
||||||
|
if (cfg[i].id == "voxel") continue; // voxel 由外部单独 setDatasets 注入,勿覆盖
|
||||||
|
if (auto* sec = section(cfg[i].id)) sec->setDatasets(b.segments[i]);
|
||||||
|
}
|
||||||
|
updatePlaceholderAndVisibility();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
并在 `section("voxel")->setDatasets(...)` 的外部调用点之后也需刷新 —— 故在 `CategorySection::setDatasets` 触发后由信号驱动:Step 5。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 数据变化驱动显隐** — 段内容重建后段的「有无行」可能变(如 voxel 注入、筛选)。在 `CategoryAnalysisTab` 构造的每段 connect 区,追加:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
connect(sec, &CategorySection::checkedDatasetsChanged, this,
|
||||||
|
[this](const QStringList&) { updatePlaceholderAndVisibility(); });
|
||||||
|
```
|
||||||
|
(与已有 checkedDatasetsChanged lambda 并存;或在那个 lambda 末尾调用 `updatePlaceholderAndVisibility()`。)另外在 `CategorySection::setDatasets` 末尾发一个轻量信号或直接由 tab 在调用 `section("voxel")->setDatasets()` 后手动 `updatePlaceholderAndVisibility()`。最简:在 tab 暴露 `void refreshVisibility(){ updatePlaceholderAndVisibility(); }` 供 main 在注入 voxel 数据后调用。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 构建 + 手动验收** — `build.bat app`。验收:无数据时面板显示占位语;勾选含轨迹/反演的测线后,对应段出现、占位消失;不含某类型时该段不显示。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/columns/CategoryAnalysisTab.hpp src/app/panels/columns/CategoryAnalysisTab.cpp src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp
|
||||||
|
git commit -m "feat(vtk): 数据集单栏 段按数据动态显隐 + 全空占位提示"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task B2: ColumnDrawer 去 tab + 单列承载 + 接 2D 数据
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/panels/columns/ColumnDrawer.hpp/.cpp`
|
||||||
|
- Modify: `src/app/main.cpp`(drawer 接线:`670`、`1388-1415`、`1710-1713`、`544-566`、`analysisModeChanged` 链)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `CategoryAnalysisTab*`(统一列)。
|
||||||
|
- Produces: `ColumnDrawer` 不再有 `col2D()` / `analysisModeChanged`;只暴露 `analysisTab()`。main 把 2D 数据并入 `splitByCategory` 流。
|
||||||
|
|
||||||
|
- [ ] **Step 1: ColumnDrawer 去 QTabWidget/Column2DDataset** — `ColumnDrawer.hpp`:删 `Column2DDataset* col2D_`、`col2D()`、`analysisModeChanged`;`body_` 改为直接持 `CategoryAnalysisTab*`(不再是 QTabWidget)。`.cpp` 构造:把 `analysisTab_` 直接作为 body 内容(去掉 tab 添加 col2D 的代码、去掉 tab 切换发 `analysisModeChanged` 的 connect),折叠开关逻辑不变(折叠隐藏 body)。`resizeEvent` 里两 tab 平分逻辑删除(只剩单列)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: main.cpp 删 col2D 接线** — 删除 `main.cpp:670` 的 `drawer->col2D()->setDatasets(...)`;删除 `1388-1415` 的 `basemapChanged`/`view2DModeChanged`/`customZChanged`/`checkedDatasetsChanged`(col2D) 接线(其去向在 Phase D/E/F 重接);删除 `544-566` 的 `selectedDatasetsChanged`/`setSelectedMapLines` 经 col2D 的链(2D 选中改由统一段 `datasetSelected` 承担,已存在);删除 `analysisModeChanged` → `setAnalysisMode2D` 的接线。
|
||||||
|
|
||||||
|
- [ ] **Step 3: main.cpp 2D 数据并入单列** — 找到喂 buckets 的位置(`setBuckets(splitByCategory(...))` 调用点),确认 `splitByCategory(*lastSourceRows)` 现已含 trajectory 段(Task A1),故 2D 轨迹随 `setBuckets` 自动进 trajectory 段,无需单独 col2D 注入。删 `splitByDimension(...).dim2D` 用法(保留 `dimOf` 给渲染路由)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 2D 勾选 → 渲染接线** — 统一段的 `checkedDatasetsChanged` 现合并 2D+3D 勾选并集上抛。需在控制器侧按 ddCode 分派:3D→`setChecked3D`、2D→`setChecked2DDatasets`。**本步仅接线让 2D 段勾选仍走 `setChecked2DDatasets`**:在 main 接收 `analysisTab` 的勾选并集后,用 `dimOf(ddCode)` 拆分为 2D/3D 两组分别下发(拆分依据 ddCode,需 main 持有 dsId→ddCode 映射,复用现有 `lastSourceRows`)。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// main.cpp:analysisTab.checkedDatasetsChanged 接收后拆分下发
|
||||||
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::checkedDatasetsChanged, &window,
|
||||||
|
[sceneCtrl, lastSourceRows](const QStringList& ids) {
|
||||||
|
QStringList ids3d, ids2d;
|
||||||
|
for (const QString& id : ids) {
|
||||||
|
const std::string ddc = ddCodeOf(*lastSourceRows, id.toStdString()); // 小工具:按 id 查 ddCode
|
||||||
|
(geopro::app::dimOf(ddc) == geopro::app::Dim::D2 ? ids2d : ids3d) << id;
|
||||||
|
}
|
||||||
|
sceneCtrl->setChecked3DDatasets(ids3d); // 现 setChecked 改名/封装为 3D 专用
|
||||||
|
sceneCtrl->setChecked2DDatasets(ids2d);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
(`dimOf` 现为 `DatasetDimension.cpp` 匿名命名空间内部函数 → 提升为头文件可见的 `geopro::app::dimOf(const std::string&)`;`ddCodeOf` 为 main 内 lambda:遍历 `*lastSourceRows` 找 `r.id==id` 返回 `r.ddCode`。`setChecked3DDatasets` = 现 `VtkSceneController::setChecked` 重命名,仅语义澄清。)
|
||||||
|
|
||||||
|
- [ ] **Step 5: 构建 + 手动验收** — `build.bat app`。验收:左侧只剩一个单列(无 tab);勾 3D 反演/体 → 渲染体/帘面;勾轨迹 → 渲染足迹折线(落点暂沿用旧 placement,Phase E 改平面);无崩溃。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/columns/ColumnDrawer.hpp src/app/panels/columns/ColumnDrawer.cpp src/app/main.cpp src/app/DatasetDimension.hpp src/app/DatasetDimension.cpp src/controller/VtkSceneController.hpp src/controller/VtkSceneController.cpp
|
||||||
|
git commit -m "refactor(vtk): 抽屉去 tab 改单列; 2D/3D 勾选按维度拆分下发"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task B3: 退役 Column2DDataset
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Delete: `src/app/panels/columns/Column2DDataset.hpp/.cpp`
|
||||||
|
- Modify: `CMakeLists.txt`(移除其源文件登记)、其余 include 引用
|
||||||
|
|
||||||
|
- [ ] **Step 1: 确认无引用** — `grep -rn "Column2DDataset" src` 应只剩将删的文件自身。若 main/其他仍引用,先清掉。
|
||||||
|
- [ ] **Step 2: 删文件 + CMake 登记** — 删两文件;在 `CMakeLists.txt` 移除 `Column2DDataset.cpp`。
|
||||||
|
- [ ] **Step 3: 构建** — `build.bat app` 通过。
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -u
|
||||||
|
git commit -m "refactor(vtk): 移除退役的 Column2DDataset(并入统一单列)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C:响应式图标工具条 + 筛选折叠 + 新增三维体图标化
|
||||||
|
|
||||||
|
### Task C1: SectionIconBar 溢出计算(纯逻辑 TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/app/panels/columns/SectionIconBar.hpp/.cpp`
|
||||||
|
- Create: `tests/app/test_section_icon_bar.cpp`
|
||||||
|
- Modify: `tests/CMakeLists.txt`、`CMakeLists.txt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: 自由函数 `int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons);` —— 返回在「最多 `maxIcons`、每图标 `iconPx`、溢出按钮 `overflowPx`、可用宽 `availablePx`」下能显示的图标数(其余收进「…」)。规则:先取 `min(totalIcons, maxIcons)` 个的理想数;若 `理想数 * iconPx > availablePx`,逐个减少直到 `n*iconPx + (有溢出时 overflowPx) <= availablePx`,至少 0;当有图标被折叠时需为「…」预留 `overflowPx`。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试** — `tests/app/test_section_icon_bar.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
|
|
||||||
|
using geopro::app::visibleIconCount;
|
||||||
|
|
||||||
|
TEST(SectionIconBar, ShowsAllWhenWideEnoughAndUnderMax) {
|
||||||
|
// 3 图标, 宽 1000, 每个 30, 溢出 30, max 3 → 全显
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 1000, 30, 30, 3), 3);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, CapsAtMaxIcons) {
|
||||||
|
// 5 图标但 max 3, 宽足够 → 显 3, 其余 2 进溢出(此时需留溢出位)
|
||||||
|
EXPECT_EQ(visibleIconCount(5, 1000, 30, 30, 3), 3);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, FoldsRightWhenNarrow) {
|
||||||
|
// 3 图标, max 3, 但宽只够 2 个 + 溢出: 75px, 30 each, overflow 30 → 2*30+30=90>75 → 1*30+30=60<=75 → 1
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 75, 30, 30, 3), 1);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, NoOverflowReserveWhenAllFit) {
|
||||||
|
// 2 图标全显且 <=max, 不需溢出位: 宽 60 恰好 2*30
|
||||||
|
EXPECT_EQ(visibleIconCount(2, 60, 30, 30, 3), 2);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, ZeroWhenTooNarrow) {
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 20, 30, 30, 3), 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测试确认失败** — `build.bat test`;预期编译失败(头文件/函数不存在)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 `SectionIconBar.hpp`(函数 + 组件声明)**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#pragma once
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QString>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class QToolButton;
|
||||||
|
class QResizeEvent;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 纯逻辑:给定约束返回可见图标数(其余收进「…」)。见 spec §6。
|
||||||
|
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons);
|
||||||
|
|
||||||
|
// 段头响应式图标工具条:≤maxIcons 且宽度够则全显;否则右侧依次收进末尾「…」下拉。
|
||||||
|
struct IconAction {
|
||||||
|
QString glyphKey; // 图标键(映射到 Glyph)
|
||||||
|
QString tooltip;
|
||||||
|
std::function<void()> onClick; // 直接动作;为空则用 popupBuilder
|
||||||
|
std::function<void(QToolButton*)> popupBuilder; // 弹 popup(z值/底图/筛选用)
|
||||||
|
};
|
||||||
|
|
||||||
|
class SectionIconBar : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SectionIconBar(QWidget* parent = nullptr);
|
||||||
|
void setActions(const std::vector<IconAction>& actions); // 重建按钮
|
||||||
|
void setMaxIcons(int n) { maxIcons_ = n; relayout(); }
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent* e) override;
|
||||||
|
private:
|
||||||
|
void relayout(); // 按当前宽度算可见数,多余进「…」菜单
|
||||||
|
std::vector<IconAction> actions_;
|
||||||
|
std::vector<QToolButton*> btns_;
|
||||||
|
QToolButton* overflowBtn_ = nullptr;
|
||||||
|
int maxIcons_ = 3;
|
||||||
|
int iconPx_ = 30;
|
||||||
|
int overflowPx_ = 30;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 实现 `SectionIconBar.cpp` 的 `visibleIconCount`(先只够过测试)**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons) {
|
||||||
|
if (totalIcons <= 0 || iconPx <= 0) return 0;
|
||||||
|
const int ideal = std::min(totalIcons, std::max(0, maxIcons));
|
||||||
|
const bool overflowFromCap = totalIcons > ideal; // 超 max → 必有溢出位
|
||||||
|
// 先看理想数能否放下(若已因 cap 溢出,理想数也要含溢出位)
|
||||||
|
auto fits = [&](int n, bool withOverflow) {
|
||||||
|
return n * iconPx + (withOverflow ? overflowPx : 0) <= availablePx;
|
||||||
|
};
|
||||||
|
int n = ideal;
|
||||||
|
bool overflow = overflowFromCap;
|
||||||
|
while (n > 0 && !fits(n, overflow || (totalIcons > n))) {
|
||||||
|
--n;
|
||||||
|
overflow = true; // 一旦减少必有「…」
|
||||||
|
}
|
||||||
|
if (n < 0) n = 0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 跑测试确认通过** — `build.bat test`;预期 `SectionIconBar.*` PASS(登记测试见 Step 6)。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 登记到 CMake** — `CMakeLists.txt` 加 `SectionIconBar.cpp` 到 app 源;`tests/CMakeLists.txt` 加 `test_section_icon_bar.cpp`(参照 `test_dataset_category.cpp` 登记法)。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 实现组件 `relayout`/`setActions`/`resizeEvent`**(UI,构建验证):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
SectionIconBar::SectionIconBar(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QHBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
}
|
||||||
|
void SectionIconBar::setActions(const std::vector<IconAction>& a) {
|
||||||
|
actions_ = a;
|
||||||
|
// 清旧按钮、按 actions_ 建 QToolButton(autoRaise + glyph + tooltip + 连点击/popup),
|
||||||
|
// 末尾建 overflowBtn_(InstantPopup, 文本「…」);relayout()。
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
void SectionIconBar::resizeEvent(QResizeEvent* e) { QWidget::resizeEvent(e); relayout(); }
|
||||||
|
void SectionIconBar::relayout() {
|
||||||
|
const int vis = visibleIconCount(static_cast<int>(actions_.size()), width(), iconPx_, overflowPx_, maxIcons_);
|
||||||
|
// 前 vis 个按钮 show,其余 hide 并塞进 overflowBtn_ 的菜单;vis==actions_.size() 时 overflowBtn_ 隐藏。
|
||||||
|
}
|
||||||
|
```
|
||||||
|
(具体按钮构造参照 `VtkViewToolbar` 的 `iconBtn` 与 `Glyphs.hpp::makeGlyph`;popup 用 `QMenu`/`QWidgetAction`。)
|
||||||
|
|
||||||
|
- [ ] **Step 8: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/columns/SectionIconBar.hpp src/app/panels/columns/SectionIconBar.cpp tests/app/test_section_icon_bar.cpp tests/CMakeLists.txt CMakeLists.txt
|
||||||
|
git commit -m "feat(vtk): 段头响应式图标工具条 SectionIconBar(溢出折叠+单元测试)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task C2: CategorySection 段头接入图标条 + 筛选折叠 + 新增三维体图标化
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/panels/columns/CategorySection.cpp:37-132`(段头 + 筛选行)、`CategorySection.hpp`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `SectionIconBar`、`spec_.dim`、`spec_.canGenerateVolume`。
|
||||||
|
- Produces: 段头右侧为 `SectionIconBar`;筛选行默认折叠,由「筛选」图标 toggle;「新增三维体」改图标(保留 `generateVolumeRequested`)。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 段头去文字按钮、加 SectionIconBar** — 替换 `CategorySection.cpp:66-111`(新增三维体 + 导入雷达文字按钮)为:构建 `SectionIconBar* iconBar_`,按 `spec_` 组装 actions:
|
||||||
|
- 通用:`{筛选}` → toggle `filterRow_` 可见。
|
||||||
|
- `spec_.canGenerateVolume`:前插 `{新增三维体}` → `emit generateVolumeRequested(dsTypeCode, checkedDsIds())`。
|
||||||
|
- `spec_.dim==D2`:`{z值, 筛选, 底图}`(z值/底图 popup 在 Phase E/F 接,先占位发信号 `zSliderRequested()`/`basemapPopupRequested()`)。
|
||||||
|
- **不再有导入雷达**(Phase D 移走)。
|
||||||
|
添加到 `hl->addWidget(iconBar_)`。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 筛选行默认折叠** — `CategorySection.cpp:119-132` 的 `filterRow` 包进一个 `QWidget* filterRow_`,默认 `hide()`;「筛选」图标点击 toggle 其可见。`hasArrayTypeFilter` 仍只对 ERT 段建 arrayCombo。
|
||||||
|
|
||||||
|
- [ ] **Step 3: hpp 增成员/信号** — `CategorySection.hpp` 加 `SectionIconBar* iconBar_=nullptr; QWidget* filterRow_=nullptr;`;新增信号 `void zSliderRequested(); void basemapPopupRequested();`(Phase E/F 用)。移除 `radarImportRequested`(Phase D 删其发射)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:反演段头显示 [新增三维体, 筛选] 图标;轨迹段显示 [z值, 筛选, 底图];点筛选展开/收起筛选行;缩窄左栏宽度 → 右侧图标折进「…」、放宽弹回。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp
|
||||||
|
git commit -m "feat(vtk): 段头改图标工具条 + 筛选默认折叠 + 新增三维体图标化"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D:导入雷达移顶部菜单 + 3D 底图移渲染工具栏
|
||||||
|
|
||||||
|
### Task D1: 导入雷达移到 TopBar 设备菜单
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/TopBar.cpp`(`buildDeviceMenu`)、`src/app/TopBar.hpp`(新信号)
|
||||||
|
- Modify: `src/app/main.cpp:1056-1059`(接线改源)
|
||||||
|
- Modify: `CategorySection.hpp/.cpp`、`CategoryAnalysisTab.hpp/.cpp`(移除 radarImportRequested 转发,若 Task C2 未删尽)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `TopBar` 新信号 `void radarImportRequested(bool impulse);`;设备菜单含「导入雷达测线 → 规范化/Impulse」。
|
||||||
|
|
||||||
|
- [ ] **Step 1: TopBar 设备菜单加项** — 在 `buildDeviceMenu(this)`(定位其定义 TU)内追加子菜单:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QMenu* radar = menu->addMenu(QStringLiteral("导入雷达测线"));
|
||||||
|
radar->addAction(QStringLiteral("规范化测线目录(.head/.data)…"), this,
|
||||||
|
[this] { emit radarImportRequested(false); });
|
||||||
|
radar->addAction(QStringLiteral("Impulse 测线目录(.iprb)…"), this,
|
||||||
|
[this] { emit radarImportRequested(true); });
|
||||||
|
```
|
||||||
|
(`TopBar.hpp` signals 区加 `void radarImportRequested(bool impulse);`。若 `buildDeviceMenu` 是自由函数无法 emit 成员信号,则改为 TopBar 成员方法 `buildDeviceMenu()`,参照已有 `buildViewMenu()` 成员法。)
|
||||||
|
|
||||||
|
- [ ] **Step 2: main 接线改源** — `main.cpp:1056-1059` 把 `analysisTab.radarImportRequested` 改为 `topBar->radarImportRequested`(导入流程目标不变)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 删 CategorySection/Tab 的 radarImportRequested** — 移除 `CategorySection.cpp:88-111` 残留(Task C2 应已删按钮)、`CategorySection.hpp:54` 信号、`CategoryAnalysisTab.cpp:50-51` 与 `.hpp:42` 的转发。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:顶部「设备」菜单出现「导入雷达测线」二级项,点击走原导入流程;三维体段头无导入雷达按钮。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/TopBar.hpp src/app/TopBar.cpp src/app/main.cpp src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp src/app/panels/columns/CategoryAnalysisTab.hpp src/app/panels/columns/CategoryAnalysisTab.cpp
|
||||||
|
git commit -m "feat(vtk): 导入雷达入口移到顶部设备菜单(临时测试功能集中化)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task D2: TileBasemap 透明度参数化 + 隐藏 API
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/TileBasemap.hpp`、`src/app/TileBasemap.cpp:59`(`kTerrainOpacity`)、`buildFlat`/`buildWarped` opacity 行(~455、~569)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `TileBasemap` 新增 `void setOpacity(double o);`(默认 0.5);现有 `show(Kind)`/`hide()` 不变。`buildFlat`/`buildWarped` 用成员 `opacity_` 而非常量。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 加成员 + setter** — `TileBasemap.hpp` private 加 `double opacity_ = 0.5;`;public 加 `void setOpacity(double o);`。`.cpp` 实现:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void TileBasemap::setOpacity(double o) {
|
||||||
|
o = std::clamp(o, 0.0, 1.0);
|
||||||
|
if (o == opacity_) return;
|
||||||
|
opacity_ = o;
|
||||||
|
if (kind_ != Hidden) show(kind_); // 重建套用新透明度
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 渲染用 opacity_** — `buildFlat`(~455)与 `buildWarped`(~569)的 `SetOpacity(kTerrainOpacity)` 改 `SetOpacity(opacity_)`;删除或保留 `kTerrainOpacity` 常量(改为默认初值来源,建议删常量、成员默认 0.5)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 构建** — `build.bat app` 通过(行为暂不变,opacity 默认 0.5 略比旧 0.55 透)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/TileBasemap.hpp src/app/TileBasemap.cpp
|
||||||
|
git commit -m "refactor(vtk): TileBasemap 透明度参数化(默认0.5)备多实例与可调"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task D3: VtkViewToolbar 加「地图」按钮 + popup,接 3D 底图
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/VtkViewToolbar.hpp/.cpp:55-58`(Gear 之后)
|
||||||
|
- Modify: `src/app/main.cpp:1388-1394`(basemap 接线改到工具栏)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `VtkViewToolbar` 新信号 `void basemapKindChanged(int kind); void basemapOpacityChanged(double o);`(kind: 0 天地图 / 1 无)。
|
||||||
|
|
||||||
|
- [ ] **Step 1: Gear 下加地图按钮 + popup** — `VtkViewToolbar.cpp` 在 `connect(iconBtn(Glyph::Gear,...))` 之后、`sep();` 之前,加:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
{
|
||||||
|
auto* mapBtn = iconBtn(Glyph::Map, QStringLiteral("底图")); // 若无 Glyph::Map 用近义图标
|
||||||
|
mapBtn->setPopupMode(QToolButton::InstantPopup);
|
||||||
|
auto* menu = new QMenu(mapBtn);
|
||||||
|
// 底图类型
|
||||||
|
menu->addAction(QStringLiteral("天地图"), this, [this]{ emit basemapKindChanged(0); });
|
||||||
|
menu->addAction(QStringLiteral("无"), this, [this]{ emit basemapKindChanged(1); });
|
||||||
|
menu->addSeparator();
|
||||||
|
// 透明度滑块(QWidgetAction 承载 QSlider 0..100 默认 50)
|
||||||
|
auto* wa = new QWidgetAction(menu);
|
||||||
|
auto* sld = new QSlider(Qt::Horizontal, menu);
|
||||||
|
sld->setRange(0, 100); sld->setValue(50);
|
||||||
|
connect(sld, &QSlider::valueChanged, this, [this](int v){ emit basemapOpacityChanged(v/100.0); });
|
||||||
|
wa->setDefaultWidget(sld);
|
||||||
|
menu->addAction(wa);
|
||||||
|
mapBtn->setMenu(menu);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
(`VtkViewToolbar.hpp` signals 区加两信号;include `<QMenu> <QWidgetAction> <QSlider>`。`Glyph::Map` 不存在则在 `Glyphs.hpp` 选已有近义键。)
|
||||||
|
|
||||||
|
- [ ] **Step 2: main 接线** — `main.cpp:1388-1394` 原 `col2D basemapChanged` 接线(Task B2 已删)在此重建为工具栏信号:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QObject::connect(toolbar, &geopro::app::VtkViewToolbar::basemapKindChanged, basemap,
|
||||||
|
[basemap, basemapKind](int kind) {
|
||||||
|
*basemapKind = (kind == 0) ? geopro::app::TileBasemap::Satellite
|
||||||
|
: geopro::app::TileBasemap::Hidden;
|
||||||
|
basemap->show(*basemapKind);
|
||||||
|
});
|
||||||
|
QObject::connect(toolbar, &geopro::app::VtkViewToolbar::basemapOpacityChanged, basemap,
|
||||||
|
[basemap](double o) { basemap->setOpacity(o); });
|
||||||
|
```
|
||||||
|
(`toolbar` = 现有 `VtkViewToolbar` 实例指针,按 main 里实际变量名接。)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 去 setAnalysisMode2D 6 向禁用** — `VtkViewToolbar::setAnalysisMode2D` 不再被调用(Phase B 已断 analysisModeChanged),可保留空实现或删除声明+定义+`viewDirButtons_` 禁用逻辑(建议删,单一自由场景 6 向恒可用)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:渲染区工具栏 Gear 下方有「底图」按钮;点开可切天地图/无、拖透明度实时变化;3D 底图为全局唯一。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/VtkViewToolbar.hpp src/app/VtkViewToolbar.cpp src/app/main.cpp
|
||||||
|
git commit -m "feat(vtk): 3D 底图控件移渲染区工具栏(天地图/无+透明度滑块)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E:单一自由场景 + 2D 按类型平面 z
|
||||||
|
|
||||||
|
### Task E1: 移除维度相机/显隐耦合 + 废弃逐 ds 拖 Z
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/VtkSceneView.hpp/.cpp:420-439`(`setAnalysisMode2D`)、`482-539`(`pickMapLineAt`/`nudgeSelectedMapLinesZ`)
|
||||||
|
- Modify: `src/controller/VtkSceneController.*`(`onAnalysisModeChanged`、`view2DMode`/`set2DPlacement` mode 维度)
|
||||||
|
- Modify: `src/app/main.cpp`(拖 Z 浮层 `498-505`、`544-557`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: 场景恒自由透视;删除 `setAnalysisMode2D`、`nudgeSelectedMapLinesZ`、`mapLineZOffset_`、拖 Z 浮层。`set2DPlacement(mode,z)` 简化为后续平面 z 接口(Task E2 替换)。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 删 VtkSceneView 维度显隐/相机** — 移除 `setAnalysisMode2D`(420-439) 的按维度翻可见 + 相机锁定;移除 `pickMapLineAt`/`nudgeSelectedMapLinesZ`/`mapLineZOffset_`(482-539 及成员)。`mapLineDs_` 是否保留视 Task E2 需要(仍需识别足迹 actor 归属 → 保留)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 删 controller analysisMode/view2DMode 耦合** — 移除 `onAnalysisModeChanged`、`placement2dMode_` 的 0/2/3 模式分支(顶/底/关闭);`set2DPlacement` 暂留接口待 E2 改造。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 删 main 拖 Z 浮层接线** — 移除 `main.cpp:498-505`(高程读数浮层)、`544-557`(pick/nudge 接线)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:无 tab 切换后场景恒自由透视;2D/3D 同时可见可旋转;无拖 Z 浮层;无崩溃/悬挂引用。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/VtkSceneView.hpp src/app/VtkSceneView.cpp src/controller/VtkSceneController.hpp src/controller/VtkSceneController.cpp src/app/main.cpp
|
||||||
|
git commit -m "refactor(vtk): 删维度相机/显隐耦合与逐ds拖Z(单一自由场景)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task E2: 2D 按类型平面 z 生命周期(纯逻辑 TDD)+ 控制器接入
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/controller/PlaneBasemapManager.hpp/.cpp`(先只含平面 z 生命周期逻辑;底图实例 Phase F 加)
|
||||||
|
- Create: `tests/controller/test_plane_basemap_manager.cpp`
|
||||||
|
- Modify: `tests/CMakeLists.txt`、`CMakeLists.txt`
|
||||||
|
- Modify: `src/controller/VtkSceneController.cpp`(2D 落点改用管理器平面 z)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `class PlaneZRegistry`(纯逻辑核):
|
||||||
|
- `double onChecked(const std::string& typeId, const std::string& dsId, double dsZ);` —— 某类型某 ds 勾选;首勾记录平面 z=dsZ;返回该类型当前平面 z。
|
||||||
|
- `void onUnchecked(const std::string& typeId, const std::string& dsId);` —— 取消;该类型空集时清除(平面消失)。
|
||||||
|
- `bool hasPlane(const std::string& typeId) const;`
|
||||||
|
- `double planeZ(const std::string& typeId) const;`
|
||||||
|
- `void setPlaneZ(const std::string& typeId, double z);` —— 滑块整体调。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试** — `tests/controller/test_plane_basemap_manager.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "controller/PlaneBasemapManager.hpp"
|
||||||
|
|
||||||
|
using geopro::controller::PlaneZRegistry;
|
||||||
|
|
||||||
|
TEST(PlaneZRegistry, FirstCheckSetsPlaneZ) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "a", 12.0), 12.0);
|
||||||
|
EXPECT_TRUE(r.hasPlane("trajectory"));
|
||||||
|
EXPECT_DOUBLE_EQ(r.planeZ("trajectory"), 12.0);
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, SecondCheckKeepsFirstZ) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "b", 99.0), 12.0); // 投影到首个 ds 的平面
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, PlaneDisappearsWhenAllUnchecked) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
r.onChecked("trajectory", "b", 99.0);
|
||||||
|
r.onUnchecked("trajectory", "a");
|
||||||
|
EXPECT_TRUE(r.hasPlane("trajectory")); // 还有 b
|
||||||
|
r.onUnchecked("trajectory", "b");
|
||||||
|
EXPECT_FALSE(r.hasPlane("trajectory")); // 全消 → 平面消失
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, RecheckAfterEmptyResetsZ) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
r.onUnchecked("trajectory", "a");
|
||||||
|
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "c", 7.0), 7.0); // 重新首勾 → 新 z
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, SetPlaneZMovesPlane) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
r.setPlaneZ("trajectory", 30.0);
|
||||||
|
EXPECT_DOUBLE_EQ(r.planeZ("trajectory"), 30.0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测试确认失败** — `build.bat test`;预期编译失败。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 `PlaneBasemapManager.hpp` 的 `PlaneZRegistry`**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#pragma once
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace geopro::controller {
|
||||||
|
|
||||||
|
// 纯逻辑:按 2D 类型管理「平面 z + 成员集」。首勾定 z(之后固定); 全消则平面消失。见 spec §8.2。
|
||||||
|
class PlaneZRegistry {
|
||||||
|
public:
|
||||||
|
double onChecked(const std::string& typeId, const std::string& dsId, double dsZ) {
|
||||||
|
auto& p = planes_[typeId];
|
||||||
|
if (p.members.empty()) p.z = dsZ; // 首勾定 z
|
||||||
|
p.members.insert(dsId);
|
||||||
|
return p.z;
|
||||||
|
}
|
||||||
|
void onUnchecked(const std::string& typeId, const std::string& dsId) {
|
||||||
|
auto it = planes_.find(typeId);
|
||||||
|
if (it == planes_.end()) return;
|
||||||
|
it->second.members.erase(dsId);
|
||||||
|
if (it->second.members.empty()) planes_.erase(it); // 全消 → 平面消失
|
||||||
|
}
|
||||||
|
bool hasPlane(const std::string& typeId) const { return planes_.count(typeId) > 0; }
|
||||||
|
double planeZ(const std::string& typeId) const {
|
||||||
|
auto it = planes_.find(typeId);
|
||||||
|
return it == planes_.end() ? 0.0 : it->second.z;
|
||||||
|
}
|
||||||
|
void setPlaneZ(const std::string& typeId, double z) {
|
||||||
|
auto it = planes_.find(typeId);
|
||||||
|
if (it != planes_.end()) it->second.z = z;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
struct Plane { double z = 0.0; std::set<std::string> members; };
|
||||||
|
std::map<std::string, Plane> planes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::controller
|
||||||
|
```
|
||||||
|
(`PlaneBasemapManager.cpp` 暂可空/仅 include;底图实例逻辑 Phase F 加。)
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑测试确认通过** — `build.bat test`;登记测试见 Step 5;预期 `PlaneZRegistry.*` PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 登记 CMake** — `CMakeLists.txt` 加 `PlaneBasemapManager.cpp`;`tests/CMakeLists.txt` 加 `test_plane_basemap_manager.cpp`。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 控制器接入平面 z** — `VtkSceneController` 持 `PlaneZRegistry planeReg_;`;`setChecked2DDatasets` diff 时:新增 ds → 需其 dsZ(初值:用 `view_.zRefElev()` 或 0,取 spec「首个勾选 ds 的 z」——首个 ds 无既有 z 则取场景地表基准 `zRefElev()`)→ `planeReg_.onChecked(typeId, dsId, zRef)` 得平面 z → `view_.addMapLine(dsId, line, planeZ)`;取消 → `planeReg_.onUnchecked` 并 `view_.removeDataset`。`typeId` = ds 所属段 id(2D 类型),由 ddCode 映射(`dd_trajectory_data`→"trajectory")。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 构建 + 手动验收** — `build.bat app`。验收:勾多条轨迹 → 全落同一平面 z(首条决定);取消首条 → 平面 z 不变;全取消 → 足迹消失。
|
||||||
|
|
||||||
|
- [ ] **Step 8: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/controller/PlaneBasemapManager.hpp src/controller/PlaneBasemapManager.cpp tests/controller/test_plane_basemap_manager.cpp tests/CMakeLists.txt CMakeLists.txt src/controller/VtkSceneController.hpp src/controller/VtkSceneController.cpp
|
||||||
|
git commit -m "feat(vtk): 2D 按类型平面 z 生命周期(首勾定z/全消消失)+单元测试"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task E3: 段「z值」滑块 popup 接平面 z
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/panels/columns/CategorySection.cpp`(z值 action 的 popup)
|
||||||
|
- Modify: `src/app/panels/columns/CategoryAnalysisTab.*`、`src/app/main.cpp`(信号转发到 controller `setPlaneZ`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `CategorySection` 信号 `void planeZChanged(const QString& typeId, double z);`;controller `void setPlaneZ(const QString& typeId, double z)` → `planeReg_.setPlaneZ` + 重摆该类型足迹。
|
||||||
|
|
||||||
|
- [ ] **Step 1: z值 popup** — Task C2 的 z值 action `popupBuilder` 内建 `QSlider`(范围按场景高程量级,如 -500..500 米,默认取当前 `planeZ`),`valueChanged` → `emit planeZChanged(typeIdOfSpec, v)`。`typeId` = `spec_.id`。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 转发链** — `CategoryAnalysisTab` 转发 `planeZChanged`;main 接到 → `sceneCtrl->setPlaneZ(typeId, z)`。
|
||||||
|
|
||||||
|
- [ ] **Step 3: controller setPlaneZ** — `planeReg_.setPlaneZ(typeId,z)` 后,对该类型已勾选足迹 `removeDataset`+`addMapLine(...,z)` 重摆(参照旧 `set2DPlacement` 重摆法)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:拖 z值滑块 → 该类型整块平面(含其上全部足迹)整体升降。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp src/app/panels/columns/CategoryAnalysisTab.hpp src/app/panels/columns/CategoryAnalysisTab.cpp src/app/main.cpp src/controller/VtkSceneController.hpp src/controller/VtkSceneController.cpp
|
||||||
|
git commit -m "feat(vtk): 2D 段 z值滑块整体升降类型平面"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F:2D 平面底图(TileBasemap 多实例)
|
||||||
|
|
||||||
|
### Task F1: TileBasemap groundZ 参数化 + 多实例可用
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/TileBasemap.hpp/.cpp`(`kGroundZ` → 成员 `groundZ_`;构造可传初值)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `TileBasemap` 构造增可选参 `double groundZ = 0.0`;内部所有 `kGroundZ` 用 `groundZ_`;`Street` 走 `buildFlat`(已是纯平矢量)不变。
|
||||||
|
|
||||||
|
- [ ] **Step 1: groundZ 成员化** — `TileBasemap.hpp` 加 `double groundZ_;` + 构造形参 `double groundZ = 0.0`;`.cpp` 把 `placeActor`/`buildFlat`/`buildWarped`/Z-fighting 偏移里出现的 `kGroundZ` 改 `groundZ_`(`gz = groundZ_ + (z-kMinZoom)*kZEps`)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 确认多实例无共享态** — review `TileBasemap` 成员:`nam_`/`placed_`/`desired_`/`demCache_`/`texCache_`/`observer_` 均 per-instance;`tileKey` static 纯函数 → 多实例安全。无需改。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 构建** — `build.bat app`(3D 底图仍 groundZ=0,行为不变)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/TileBasemap.hpp src/app/TileBasemap.cpp
|
||||||
|
git commit -m "refactor(vtk): TileBasemap groundZ 参数化支持多实例平面底图"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task F2: PlaneBasemapManager 管 N 个平面底图实例 + 段「底图」popup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/controller/PlaneBasemapManager.hpp/.cpp`(加底图实例管理)
|
||||||
|
- Modify: `src/controller/VtkSceneController.*`、`src/app/main.cpp`、`CategorySection.*`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `PlaneZRegistry`(平面 z 生命周期)、`TileBasemap`(多实例)、`Scene&`/`vtkRenderWindow*`/`GeoLocalFrame`、`dataRadiusProvider`。
|
||||||
|
- Produces: `PlaneBasemapManager`:按 typeId 持 `TileBasemap`(Street 纯平、groundZ=planeZ、opacity);`onPlaneCreated(typeId,z)` 建实例、`onPlaneDestroyed(typeId)` 销毁、`setKind(typeId,kind)`/`setOpacity(typeId,o)`/`setPlaneZ(typeId,z)`(重建实例于新 z)。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 管理器持底图实例** — `PlaneBasemapManager` 内 `std::map<std::string, std::unique_ptr<TileBasemap>> bms_;`。`onPlaneCreated`:`new TileBasemap(scene, rw, frame, groundZ=z)` → `setDataRadiusProvider(provider)` → `show(Street)`(默认矢量平面)、`setOpacity(0.5)`。`onPlaneDestroyed`:`bms_.erase(typeId)`(析构移除瓦片)。`setPlaneZ`:销毁旧实例、按新 z 重建(或加 `TileBasemap::setGroundZ` 重铺——本步用重建,简单可靠)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 串到平面生命周期** — controller 在 `PlaneZRegistry` 首勾(`hasPlane` false→true) 时 `mgr.onPlaneCreated(typeId, z)`;全消(true→false) 时 `mgr.onPlaneDestroyed(typeId)`;`setPlaneZ` 同步 `mgr.setPlaneZ`。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 段「底图」popup** — Task C2 的底图 action `popupBuilder`:菜单【矢量平面(默认)/无】+ 透明度滑块(默认50);发 `basemapKindChanged(typeId,kind)`/`basemapOpacityChanged(typeId,o)` → main → `mgr.setKind/ setOpacity`。「无」= `bms_[typeId]->hide()`(保留实例,仅隐瓦片)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:勾首条轨迹 → 出现该类型平面矢量底图(贴平面 z、纯平无高程、范围≈数据范围);z值滑块带底图同升降;底图 popup 可切无/调透明度;全取消 → 平面+底图一并消失。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/controller/PlaneBasemapManager.hpp src/controller/PlaneBasemapManager.cpp src/controller/VtkSceneController.hpp src/controller/VtkSceneController.cpp src/app/main.cpp src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp src/app/panels/columns/CategoryAnalysisTab.hpp src/app/panels/columns/CategoryAnalysisTab.cpp
|
||||||
|
git commit -m "feat(vtk): 2D 每类型平面矢量底图(TileBasemap多实例+生命周期+底图popup)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review(计划对照 spec)
|
||||||
|
|
||||||
|
**Spec coverage:**
|
||||||
|
- §4 面板单列/动态显隐/空占位 → Task B1/B2/B3 ✓
|
||||||
|
- §5 统一段配置(dimension+trajectory) → Task A1 ✓
|
||||||
|
- §6 响应式图标条+「…」溢出(数量+宽度两分支) → Task C1/C2 ✓
|
||||||
|
- §7.1 筛选折叠 → C2;§7.2 新增三维体图标 → C2;§7.3 z值 → E3;§7.4 2D底图 → F2;§7.5 3D底图移工具栏 → D2/D3;§7.6 导入雷达移设备菜单 → D1 ✓
|
||||||
|
- §8 单一自由场景 + 2D按类型平面 + 废弃逐ds拖Z/view2DMode → E1/E2/E3 ✓
|
||||||
|
- §9.1 3D共享底图(透明度可调/隐藏) → D2/D3;§9.2 N个2D平面底图(TileBasemap多实例+生命周期) → F1/F2 ✓
|
||||||
|
- §10 默认勾选保留 + 2D信号迁段 → B2/E2/E3 ✓
|
||||||
|
|
||||||
|
**Placeholder scan:** UI 接线步骤(C1-Step7、F2 等)给出真实代码骨架 + 参照锚点(`VtkViewToolbar::iconBtn`、`Glyphs.hpp`、旧 `set2DPlacement` 重摆法),非 TODO。纯逻辑任务(A1/C1/E2)含完整测试与实现代码。
|
||||||
|
|
||||||
|
**Type consistency:** `CategoryDim`(A1) / `visibleIconCount`(C1) / `PlaneZRegistry`(E2) / `TileBasemap::setOpacity`(D2)·`groundZ`(F1) / `setChecked3DDatasets`·`setChecked2DDatasets`·`setPlaneZ`(B2/E2/E3) 跨任务名称一致。
|
||||||
|
|
||||||
|
**已知待实现期细化(非占位,属合理实现细节):** z值滑块数值范围(E3,按场景高程量级);`Glyph::Map` 若不存在选近义键(D3);`buildDeviceMenu` 自由函数 vs 成员法(D1,按实际定义形态)。
|
||||||
Loading…
Reference in New Issue