feat/vtk-merged-dataset-column #10

Merged
gaozheng merged 40 commits from feat/vtk-merged-dataset-column into main 2026-07-01 14:48:38 +08:00
1 changed files with 388 additions and 131 deletions
Showing only changes of commit 7d9f34d3ec - Show all commits

View File

@ -23,120 +23,313 @@
## 文件结构(创建/修改一览)
**新建**
**新建(抽象层 —— spec §5**
- `src/data/repo/CategoryDescriptor.hpp/.cpp``CategoryDescriptor` + `SceneKind`/`FilterKind`/`OpKind` 枚举 + `byDdCode`/`byDsTypeCode` 便捷器 + `categoryCatalog()`(取代 `CategoryConfig.hpp`/`categoryConfigs`)。
- `src/controller/DatasetRenderStrategy.hpp/.cpp``IDatasetRenderStrategy` 接口 + 字符串键注册表 + 3 实现(`VolumeRenderStrategy`/`CurtainRenderStrategy`/`Plane2DRenderStrategy`)。`Plane2DRenderStrategy` 内含 `PlaneZRegistry`(平面 z 生命周期)+Phase FN 个 `TileBasemap` 平面底图。
- `src/app/panels/columns/SectionIconBar.hpp/.cpp` — 段头响应式图标工具条默认≤3超出/挤压收进「…」)。
- `src/controller/PlaneBasemapManager.hpp/.cpp` — 按 2D 类型管理「平面 z + 平面底图实例」生命周期(首勾建/全消销毁)。
- `tests/data/test_category_descriptor.cpp` — classify 路由 + catalog 完整性纯逻辑测试。
- `tests/controller/test_plane_z_registry.cpp` — 平面 z 生命周期纯逻辑测试(`PlaneZRegistry`)。
- `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/DatasetCategory.hpp/.cpp` — `splitByCategory` 改为遍历 `categoryCatalog()``classify` 路由(取代 ddCode/dsTypeCode 硬匹配)
- `src/app/panels/columns/CategoryAnalysisTab.hpp/.cpp`统一单列 `DatasetColumn`:动态显隐 + 空占位 + 遍历 catalog 建段。
- `src/app/panels/columns/CategorySection.hpp/.cpp`构造改吃 `CategoryDescriptor`;段头按 `operations(OpKind)``SectionIconBar`(含 `OpKind→UI` 映射一处);按 `filters(FilterKind)` 建筛选器(`FilterKind→UI` 映射一处);筛选折叠;移除导入雷达。
- `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/controller/VtkSceneController.hpp/.cpp`主流程改「查描述符 → `renderStrategyId` → 策略 `add/remove` + 首勾/全消 `onTypeActivated/Deactivated`」;移除 `view2DMode`/`setAnalysisMode2D`/`set2DPlacement` 维度耦合。
- `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/main.cpp` — 接线改造(单列喂数据、导入雷达接 TopBar、3D 底图接工具栏、删 col2D/analysisMode 链;勾选并集直接下发控制器,按描述符路由策略,无 dimOf 分判)。
- `CMakeLists.txt`、`tests/CMakeLists.txt` — 源/测试登记。
**删除(评估后)**
- `src/app/panels/columns/Column2DDataset.hpp/.cpp` — 合并后退役Phase B 末确认无引用再删)。
- `src/data/repo/CategoryConfig.hpp`、`src/app/DatasetDimension.hpp/.cpp` — 由 `CategoryDescriptor`/catalog + 策略取代后退役(确认无引用再删)。
---
## 阶段总览
- **Phase A**段配置加维度 + trajectory 段分流(纯逻辑 TDD
- **Phase B**:单列面板骨架(动态显隐 + 空占位 + 去 tab。构建后 2D/3D 同列可见。
- **Phase C**:响应式图标工具条 + 筛选折叠 + 新增三维体图标化。
- **Phase A**类型抽象基础 —— 描述符目录 `categoryCatalog`A1+ 渲染策略接口/注册表/3 骨架策略A2。纯逻辑 TDD
- **Phase B**:单列面板骨架(动态显隐 + 空占位 + 去 tab,勾选经描述符路由策略。构建后 2D/3D 同列可见。
- **Phase C**:响应式图标工具条(由 `operations`/`filters` 驱动)+ 筛选折叠 + 新增三维体图标化。
- **Phase D**:导入雷达移顶部设备菜单 + 3D 底图移渲染工具栏(含 `TileBasemap` 透明度参数化)。
- **Phase E**:单一自由场景 + 2D 按类型平面 z(纯逻辑 TDD + 接线)。
- **Phase F**2D 平面底图(`TileBasemap` 多实例 + 管理器 + 段底图 popup
- **Phase E**:单一自由场景 + `Plane2DRenderStrategy` 装入平面 z 生命周期(纯逻辑 TDD + 接线)。
- **Phase F**2D 平面底图(`TileBasemap` 多实例并入 `Plane2DRenderStrategy` + 段底图 popup
每个 Phase 结束都应 `build.bat test` 绿 + `build.bat app` 可运行。
---
## Phase A段配置 + 维度分流
## Phase A类型抽象基础(描述符目录 + 渲染策略注册表)
### Task A1: CategorySpec 加 dimension + trajectory 段 + splitByCategory 路由
### Task A1: CategoryDescriptor + categoryCatalog + classify取代 CategorySpec/categoryConfigs
**Files:**
- Modify: `src/data/repo/CategoryConfig.hpp`
- Modify: `src/app/DatasetCategory.cpp:5-20`
- Test: `tests/app/test_dataset_category.cpp`
- Create: `src/data/repo/CategoryDescriptor.hpp`、`src/data/repo/CategoryDescriptor.cpp`
- Modify: `src/app/DatasetCategory.cpp:5-20``splitByCategory` 改遍历 catalog 用 classify
- Modify: `CMakeLists.txt`(登记 `CategoryDescriptor.cpp`)、`tests/CMakeLists.txt`
- Create: `tests/data/test_category_descriptor.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 段。
- Producesspec §5.1/§5.2`namespace geopro::data` 内 `enum class SceneKind { Volume3D, Curtain3D, Plane2D };`、`enum class FilterKind { DateRange, ArrayType };`、`enum class OpKind { GenerateVolume, Filter, PlaneZ, Basemap };`、`struct CategoryDescriptor{ id,title,sceneKind,classify,filters,operations,renderStrategyId }`、`byDdCode(...)`/`byDsTypeCode(...)`、`const std::vector<CategoryDescriptor>& categoryCatalog();`。
- `splitByCategory(rows)` 返回 `CategoryBuckets`(与 catalog 同序同长),每行归入首个 `classify(row)==true` 的段。
- [ ] **Step 1: 写失败测试**`tests/app/test_dataset_category.cpp` 末尾追加
- [ ] **Step 1: 写失败测试**`tests/data/test_category_descriptor.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");
#include <gtest/gtest.h>
#include "repo/CategoryDescriptor.hpp"
#include "DatasetCategory.hpp"
using namespace geopro::data;
TEST(CategoryCatalog, HasFiveSegmentsInOrder) {
const auto& cat = categoryCatalog();
ASSERT_EQ(cat.size(), 5u);
EXPECT_EQ(cat[0].id, "resistivity");
EXPECT_EQ(cat[3].id, "voxel");
EXPECT_EQ(cat[4].id, "trajectory");
EXPECT_EQ(cat[4].sceneKind, SceneKind::Plane2D);
EXPECT_EQ(cat[4].renderStrategyId, "plane2d");
}
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);
TEST(CategoryCatalog, TrajectoryClassifiesByDdCode) {
const auto& cat = categoryCatalog();
DsRow traj; traj.id = "t1"; traj.ddCode = "dd_trajectory_data";
EXPECT_TRUE(cat[4].classify(traj));
DsRow vox; vox.ddCode = "dd_voxel";
EXPECT_FALSE(cat[4].classify(vox));
}
TEST(SplitByCategory, RoutesRowToFirstMatchingDescriptor) {
DsRow traj; traj.id = "t1"; traj.ddCode = "dd_trajectory_data";
DsRow ert; ert.id = "e1"; ert.dsTypeCode = "ERT platform inversion data";
auto b = geopro::app::splitByCategory({traj, ert});
const auto& cat = categoryCatalog();
ASSERT_EQ(b.segments.size(), cat.size());
EXPECT_EQ(b.segments[0].size(), 1u); // resistivity ← ert
EXPECT_EQ(b.segments[0][0].id, "e1");
EXPECT_EQ(b.segments[4].size(), 1u); // trajectory ← traj
EXPECT_EQ(b.segments[4][0].id, "t1");
}
```
- [ ] **Step 2: 跑测试确认失败**`build.bat test`;预期 `test_dataset_category` 编译失败(`CategoryDim` 未定义 / `dim` 成员不存在)。
- [ ] **Step 2: 跑测试确认失败**`build.bat test`;预期编译失败(`CategoryDescriptor.hpp` 不存在)。
- [ ] **Step 3: 改 `CategoryConfig.hpp`** — 在 `namespace geopro::app {``struct CategorySpec` 上方加枚举,并给 struct 加成员、给 4 个现有段补 `dim`、追加 trajectory 段:
- [ ] **Step 3: 写 `CategoryDescriptor.hpp`**
```cpp
enum class CategoryDim { D3, D2 }; // 段维度:三维(体/帘面) / 二维(轨迹/平面)
#pragma once
#include <functional>
#include <initializer_list>
#include <string>
#include <vector>
#include "repo/RepoTypes.hpp" // DsRow
struct CategorySpec {
namespace geopro::data {
enum class SceneKind { Volume3D, Curtain3D, Plane2D }; // 渲染语义/共存规则
enum class FilterKind { DateRange, ArrayType }; // 筛选器契约(可扩展)
enum class OpKind { GenerateVolume, Filter, PlaneZ, Basemap }; // 段操作契约(可扩展)
struct CategoryDescriptor {
std::string id;
std::string title;
std::string dsTypeCode;
std::string ddCode;
bool canGenerateVolume;
bool hasArrayTypeFilter;
CategoryDim dim; // 维度(图标集/渲染路由用)
SceneKind sceneKind;
std::function<bool(const DsRow&)> classify; // 轴1 数据来源/分类
std::vector<FilterKind> filters; // 轴2 筛选器
std::vector<OpKind> operations; // 轴3 段头图标操作
std::string renderStrategyId; // 轴4 渲染策略键
};
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},
// classify 便捷构造器(常见按 ddCode / dsTypeCode 接入)
std::function<bool(const DsRow&)> byDdCode(std::initializer_list<std::string> codes);
std::function<bool(const DsRow&)> byDsTypeCode(std::initializer_list<std::string> codes);
const std::vector<CategoryDescriptor>& categoryCatalog();
} // namespace geopro::data
```
- [ ] **Step 4: 写 `CategoryDescriptor.cpp`**
```cpp
#include "repo/CategoryDescriptor.hpp"
#include <vector>
namespace geopro::data {
std::function<bool(const DsRow&)> byDdCode(std::initializer_list<std::string> codes) {
std::vector<std::string> cs(codes);
return [cs](const DsRow& r) {
for (const auto& c : cs) if (r.ddCode == c) return true;
return false;
};
return kCfg;
}
std::function<bool(const DsRow&)> byDsTypeCode(std::initializer_list<std::string> codes) {
std::vector<std::string> cs(codes);
return [cs](const DsRow& r) {
for (const auto& c : cs) if (r.dsTypeCode == c) return true;
return false;
};
}
const std::vector<CategoryDescriptor>& categoryCatalog() {
static const std::vector<CategoryDescriptor> kCat = {
{"resistivity", "电阻率数据", SceneKind::Curtain3D,
byDsTypeCode({"ERT platform inversion data"}),
{FilterKind::DateRange, FilterKind::ArrayType},
{OpKind::GenerateVolume, OpKind::Filter}, "curtain"},
{"apparent", "视电阻率数据", SceneKind::Curtain3D,
byDsTypeCode({"visual resistivity data"}),
{FilterKind::DateRange, FilterKind::ArrayType},
{OpKind::GenerateVolume, OpKind::Filter}, "curtain"},
{"transient", "瞬变电磁数据", SceneKind::Curtain3D,
byDsTypeCode({"DD TRANSIENT ELECTROMAGNETIC INVERSION"}),
{FilterKind::DateRange},
{OpKind::GenerateVolume, OpKind::Filter}, "curtain"},
{"voxel", "三维体", SceneKind::Volume3D,
byDdCode({"dd_voxel"}),
{FilterKind::DateRange},
{OpKind::Filter}, "volume"},
{"trajectory", "轨迹数据", SceneKind::Plane2D,
byDdCode({"dd_trajectory_data"}),
{FilterKind::DateRange},
{OpKind::PlaneZ, OpKind::Filter, OpKind::Basemap}, "plane2d"},
};
return kCat;
}
} // namespace geopro::data
```
- [ ] **Step 5: 改 `splitByCategory``DatasetCategory.cpp`)遍历 catalog 用 classify**
```cpp
#include "DatasetCategory.hpp"
#include "repo/CategoryDescriptor.hpp"
namespace geopro::app {
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows) {
const auto& cat = geopro::data::categoryCatalog();
CategoryBuckets b;
b.segments.resize(cat.size());
for (const auto& r : rows)
for (std::size_t i = 0; i < cat.size(); ++i)
if (cat[i].classify && cat[i].classify(r)) { b.segments[i].push_back(r); break; }
return b;
}
} // namespace geopro::app
```
**过渡策略——加新不删旧**:本任务只让 `splitByCategory` 改用 `categoryCatalog()`(输出 `CategoryBuckets` 签名/顺序不变,因 catalog 与旧 `categoryConfigs` 同 id 同序)。**旧 `CategoryConfig.hpp`/`CategorySpec`/`categoryConfigs` 暂保留**——`CategoryAnalysisTab` 构造仍按 `categoryConfigs()` 建段、`CategorySection` 仍读 `canGenerateVolume`/`hasArrayTypeFilter`故全程可编译。Phase CTask C2`CategorySection`/`CategoryAnalysisTab` 迁到吃 `CategoryDescriptor``operations`/`filters` 驱动)后,再删 `CategoryConfig.hpp`。`DatasetCategory.cpp` include 加 `repo/CategoryDescriptor.hpp`。)
- [ ] **Step 6: 登记 + 跑测试确认通过**`CMakeLists.txt``CategoryDescriptor.cpp``tests/CMakeLists.txt` 加 `test_category_descriptor.cpp`(参照 `test_dataset_category.cpp`)。`build.bat test`;预期新测试全 PASS。
- [ ] **Step 7: 提交**
```bash
git add src/data/repo/CategoryDescriptor.hpp src/data/repo/CategoryDescriptor.cpp src/app/DatasetCategory.hpp src/app/DatasetCategory.cpp tests/data/test_category_descriptor.cpp tests/CMakeLists.txt CMakeLists.txt
git commit -m "feat(vtk): 类目描述符目录 categoryCatalog(classify谓词+扩展契约)取代 categoryConfigs"
```
### Task A2: 渲染策略接口 + 字符串键注册表 + 3 骨架策略
**Files:**
- Create: `src/controller/DatasetRenderStrategy.hpp`、`src/controller/DatasetRenderStrategy.cpp`
- Modify: `CMakeLists.txt`
- Test: 注册表解析逻辑随 Phase E 接入后由集成验证;本任务以编译 + 一个最小注册/解析单元测试佐证。
- Create: `tests/controller/test_render_strategy_registry.cpp`
**Interfaces:**
- Producesspec §5.4`namespace geopro::controller` 内 `class IDatasetRenderStrategy{ virtual add(typeId,dsId); remove(dsId); onTypeActivated(typeId); onTypeDeactivated(typeId); }``class RenderStrategyRegistry{ void registerStrategy(std::string id, std::unique_ptr<IDatasetRenderStrategy>); IDatasetRenderStrategy* get(const std::string& id) const; }`。
- 3 骨架实现:`VolumeRenderStrategy` / `CurtainRenderStrategy` / `Plane2DRenderStrategy`(本任务仅建类与注册;`add/remove` 内部实现 Phase B3D 包现有分支)与 Phase E/F2D 平面+底图)填充)。
- [ ] **Step 1: 写失败测试**`tests/controller/test_render_strategy_registry.cpp`
```cpp
#include <gtest/gtest.h>
#include "controller/DatasetRenderStrategy.hpp"
using namespace geopro::controller;
namespace {
struct FakeStrategy : IDatasetRenderStrategy {
int added = 0;
void add(const std::string&, const std::string&) override { ++added; }
void remove(const std::string&) override {}
};
}
TEST(RenderStrategyRegistry, ResolvesById) {
RenderStrategyRegistry reg;
reg.registerStrategy("fake", std::make_unique<FakeStrategy>());
auto* s = reg.get("fake");
ASSERT_NE(s, nullptr);
s->add("trajectory", "d1");
EXPECT_EQ(static_cast<FakeStrategy*>(s)->added, 1);
EXPECT_EQ(reg.get("missing"), nullptr);
}
```
- [ ] **Step 4: `splitByCategory` 已按 ddCode 匹配**`DatasetCategory.cpp:12-13` 现逻辑已覆盖 `dd_trajectory_data`,因 trajectory 段有 ddCode。**无需改 `DatasetCategory.cpp` 函数体**,仅确认编译。若 `DsRow``id` 字段以外用到的成员,按实际字段名调整测试。
- [ ] **Step 2: 跑测试确认失败** — `build.bat test`;预期编译失败(头文件不存在)
- [ ] **Step 5: 跑测试确认通过**`build.bat test`;预期 `DatasetCategory.*` 全 PASS。
- [ ] **Step 3: 写 `DatasetRenderStrategy.hpp`**
```cpp
#pragma once
#include <map>
#include <memory>
#include <string>
namespace geopro::controller {
class IDatasetRenderStrategy {
public:
virtual ~IDatasetRenderStrategy() = default;
virtual void add(const std::string& typeId, const std::string& dsId) = 0;
virtual void remove(const std::string& dsId) = 0;
virtual void onTypeActivated(const std::string& /*typeId*/) {}
virtual void onTypeDeactivated(const std::string& /*typeId*/) {}
};
class RenderStrategyRegistry {
public:
void registerStrategy(std::string id, std::unique_ptr<IDatasetRenderStrategy> s) {
strategies_[std::move(id)] = std::move(s);
}
IDatasetRenderStrategy* get(const std::string& id) const {
auto it = strategies_.find(id);
return it == strategies_.end() ? nullptr : it->second.get();
}
private:
std::map<std::string, std::unique_ptr<IDatasetRenderStrategy>> strategies_;
};
} // namespace geopro::controller
```
- [ ] **Step 4: 写 3 骨架策略声明(`DatasetRenderStrategy.hpp` 续 / 或同目录头)** — 各持 `VtkSceneController&`(或所需 View/Repo 引用),`add/remove` 暂转调控制器现有方法Phase B/E/F 内迁实现):
```cpp
// 同文件内(或 controller 内部)声明,构造接 VtkSceneController& 以转调现有渲染:
class VolumeRenderStrategy : public IDatasetRenderStrategy { /* add→现 addDatasetAsync(volume 分支) */ };
class CurtainRenderStrategy : public IDatasetRenderStrategy { /* add→现 addDatasetAsync(curtain 分支) */ };
class Plane2DRenderStrategy : public IDatasetRenderStrategy { /* Phase E/F平面 z + 底图 */ };
```
(具体成员/实现在 Phase EPlane2D 装 PlaneZRegistry与 Phase B3D 两策略包现有分支)填;本步只需类存在并可注册,`.cpp` 给空/最小实现使链接通过。)
- [ ] **Step 5: 登记 + 跑测试确认通过**`CMakeLists.txt``DatasetRenderStrategy.cpp``tests/CMakeLists.txt` 加 `test_render_strategy_registry.cpp`。`build.bat test`;预期 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"
git add src/controller/DatasetRenderStrategy.hpp src/controller/DatasetRenderStrategy.cpp tests/controller/test_render_strategy_registry.cpp tests/CMakeLists.txt CMakeLists.txt
git commit -m "feat(vtk): 渲染策略接口+字符串键注册表+3骨架策略(volume/curtain/plane2d)"
```
---
@ -150,7 +343,7 @@ git commit -m "feat(vtk): 段配置加维度 + trajectory 2D 段并入 splitByCa
- Modify: `src/app/panels/columns/CategoryAnalysisTab.cpp:88-97``setBuckets`)、构造函数(加占位)
**Interfaces:**
- Consumes: `categoryConfigs()`(含 trajectory 段)。
- Consumes: `categoryCatalog()`(含 trajectory 段Task A1)。
- Produces: 段在其 bucket 为空时 `hide()`、非空 `show()`;全空时显示占位 `QLabel`。`setBuckets` 现在也分发 trajectory 段2D
> 说明:保持类名 `CategoryAnalysisTab` 不变避免大范围改名风险仅扩展职责为「统一单列」。Phase B 末在文档注释标注其新职责。
@ -207,10 +400,10 @@ bool CategorySection::hasRenderableRows() const {
```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]);
const auto& cat = geopro::data::categoryCatalog();
for (std::size_t i = 0; i < cat.size() && i < b.segments.size(); ++i) {
if (cat[i].id == "voxel") continue; // voxel 由外部单独 setDatasets 注入,勿覆盖
if (auto* sec = section(cat[i].id)) sec->setDatasets(b.segments[i]);
}
updatePlaceholderAndVisibility();
}
@ -249,32 +442,64 @@ git commit -m "feat(vtk): 数据集单栏 段按数据动态显隐 + 全空占
- [ ] **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 3: main.cpp 2D 数据并入单列** — 找到喂 buckets 的位置(`setBuckets(splitByCategory(...))` 调用点),确认 `splitByCategory(*lastSourceRows)` 现已含 trajectory 段Task A1故 2D 轨迹随 `setBuckets` 自动进 trajectory 段,无需单独 col2D 注入。删 `drawer->col2D()->setDatasets(splitByDimension(...).dim2D)` 用法。渲染路由不再用 `dimOf`/`splitByDimension`(改走描述符→策略,见 Step 4`DatasetDimension.*` 待 Phase F Step4 退役
- [ ] **Step 4: 2D 勾选 → 渲染接线** — 统一段的 `checkedDatasetsChanged` 现合并 2D+3D 勾选并集上抛。需在控制器侧按 ddCode 分派3D→`setChecked3D`、2D→`setChecked2DDatasets`。**本步仅接线让 2D 段勾选仍走 `setChecked2DDatasets`**:在 main 接收 `analysisTab` 的勾选并集后,用 `dimOf(ddCode)` 拆分为 2D/3D 两组分别下发(拆分依据 ddCode需 main 持有 dsId→ddCode 映射,复用现有 `lastSourceRows`
- [ ] **Step 4: 勾选 → 描述符路由策略(统一入口,无维度散判)** — 统一段的 `checkedDatasetsChanged` 上抛 2D+3D 勾选并集。main 用 `categoryCatalog().classify` 把每个 dsId 解析到其**描述符 id=typeId**,下发控制器统一入口 `setCheckedDatasets(dsId→typeId 列表)`;控制器 diff 后按 `catalog[typeId].renderStrategyId` 查注册表派给策略 `add/remove`,并维护「每 typeId 活跃集」在首勾/全消时调 `onTypeActivated/Deactivated`
```cpp
// main.cppanalysisTab.checkedDatasetsChanged 接收后拆分下发
// main.cpp勾选并集 → (dsId, typeId) 列表 → 控制器统一入口
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;
std::vector<std::pair<std::string, std::string>> idType; // dsId, typeId(描述符 id)
const auto& cat = geopro::data::categoryCatalog();
for (const QString& q : ids) {
const std::string id = q.toStdString();
const geopro::data::DsRow* row = findRow(*lastSourceRows, id); // main lambda: 按 id 查行
if (!row) continue;
for (const auto& d : cat)
if (d.classify && d.classify(*row)) { idType.push_back({id, d.id}); break; }
}
sceneCtrl->setChecked3DDatasets(ids3d); // 现 setChecked 改名/封装为 3D 专用
sceneCtrl->setChecked2DDatasets(ids2d);
sceneCtrl->setCheckedDatasets(idType);
});
```
`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 反演/体 → 渲染体/帘面;勾轨迹 → 渲染足迹折线(落点暂沿用旧 placementPhase E 改平面);无崩溃。
控制器侧(`VtkSceneController`,本步落地统一路由 —— 取代旧 `setChecked`/`setChecked2DDatasets`
```cpp
void VtkSceneController::setCheckedDatasets(
const std::vector<std::pair<std::string,std::string>>& idType) {
std::map<std::string,std::string> next; // dsId→typeId
for (auto& p : idType) next[p.first] = p.second;
const auto& cat = geopro::data::categoryCatalog();
auto stratOf = [&](const std::string& typeId) -> IDatasetRenderStrategy* {
for (const auto& d : cat) if (d.id == typeId) return registry_.get(d.renderStrategyId);
return nullptr;
};
// 移除:旧有新无
for (auto& [id, typeId] : checked_)
if (!next.count(id)) {
if (auto* s = stratOf(typeId)) s->remove(id);
if (--typeActive_[typeId] == 0) if (auto* s = stratOf(typeId)) s->onTypeDeactivated(typeId);
}
// 新增:新有旧无
for (auto& [id, typeId] : next)
if (!checked_.count(id)) {
if (typeActive_[typeId]++ == 0) if (auto* s = stratOf(typeId)) s->onTypeActivated(typeId);
if (auto* s = stratOf(typeId)) s->add(typeId, id);
}
checked_ = std::move(next);
view_.renderIncremental();
}
```
`registry_`=Task A2 的 `RenderStrategyRegistry`,由控制器构造时注册 3 策略;`checked_`=`std::map<dsId,typeId>`、`typeActive_`=`std::map<typeId,int>` 为新成员。`VolumeRenderStrategy`/`CurtainRenderStrategy::add` 内部转调原 `addDatasetAsync` 的体/帘面分支;`Plane2DRenderStrategy::add` 本步暂转调原 `add2DDatasetAsync`Phase E/F 改平面 z+底图)。旧 `setChecked`/`setChecked2DDatasets`/`set2DPlacement` 公有入口删除或内联进策略。)
- [ ] **Step 5: 构建 + 手动验收**`build.bat app`。验收:左侧只剩一个单列(无 tab勾 3D 反演/体 → 渲染帘面/体(经 Curtain/Volume 策略);勾轨迹 → 渲染足迹折线(经 Plane2D 策略,落点暂沿用旧 placementPhase 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 勾选按维度拆分下发"
git add src/app/panels/columns/ColumnDrawer.hpp src/app/panels/columns/ColumnDrawer.cpp src/app/main.cpp src/controller/VtkSceneController.hpp src/controller/VtkSceneController.cpp src/controller/DatasetRenderStrategy.hpp src/controller/DatasetRenderStrategy.cpp
git commit -m "refactor(vtk): 抽屉去tab单列; 勾选经描述符路由渲染策略(统一入口,无维度散判)"
```
### Task B3: 退役 Column2DDataset
@ -446,33 +671,58 @@ git add src/app/panels/columns/SectionIconBar.hpp src/app/panels/columns/Section
git commit -m "feat(vtk): 段头响应式图标工具条 SectionIconBar(溢出折叠+单元测试)"
```
### Task C2: CategorySection 段头接入图标条 + 筛选折叠 + 新增三维体图标化
### Task C2: CategorySection/CategoryAnalysisTab 迁到描述符;段头图标条由 operations/filters 驱动
> 本任务是 A1「加新不删旧」的**消费方迁移点**`CategorySection`/`CategoryAnalysisTab` 构造从吃 `CategorySpec` 改吃 `CategoryDescriptor`;段头图标由 `descriptor.operations(OpKind)` 驱动、筛选器由 `descriptor.filters(FilterKind)` 驱动。迁完后 `CategoryConfig.hpp`/`CategorySpec` 不再被引用Phase F Step4 删)。
**Files:**
- Modify: `src/app/panels/columns/CategorySection.cpp:37-132`(段头 + 筛选行)、`CategorySection.hpp`
- Modify: `src/app/panels/columns/CategorySection.hpp/.cpp`(构造改吃 `CategoryDescriptor`;段头 + 筛选行)
- Modify: `src/app/panels/columns/CategoryAnalysisTab.cpp`(构造遍历 `categoryCatalog()` 建段、传描述符)
**Interfaces:**
- Consumes: `SectionIconBar`、`spec_.dim`、`spec_.canGenerateVolume`。
- Produces: 段头右侧为 `SectionIconBar`;筛选行默认折叠,由「筛选」图标 toggle「新增三维体」改图标保留 `generateVolumeRequested`
- Consumes: `geopro::data::CategoryDescriptor``operations`/`filters`/`id`/`title`)、`SectionIconBar`。
- Produces: `CategorySection(const CategoryDescriptor& desc, dict, parent)`;段头右侧 `SectionIconBar``desc.operations` 装配;筛选器按 `desc.filters` 装配;筛选行默认折叠由 `OpKind::Filter` 图标 toggle。新增信号 `void zSliderRequested(QString typeId); void basemapPopupRequested(QString typeId);`
- [ ] **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 1: 构造改吃描述符 + `OpKind→UI` 映射建图标条**`CategorySection` 成员 `spec_` 改为 `CategoryDescriptor desc_`。替换 `CategorySection.cpp:66-111`(旧文字按钮)为:建 `SectionIconBar* iconBar_`,遍历 `desc_.operations` 经**一处映射**生成 `IconAction`
- [ ] **Step 2: 筛选行默认折叠**`CategorySection.cpp:119-132``filterRow` 包进一个 `QWidget* filterRow_`,默认 `hide()`;「筛选」图标点击 toggle 其可见。`hasArrayTypeFilter` 仍只对 ERT 段建 arrayCombo。
```cpp
std::vector<IconAction> acts;
for (geopro::data::OpKind op : desc_.operations) {
switch (op) {
case geopro::data::OpKind::GenerateVolume:
acts.push_back({"plus", "新增三维体",
[this]{ emit generateVolumeRequested(QString(), checkedDsIds()); }, {}});
break; // dsTypeCode 不再由段配置带,改由 generateVolumeRequested 接收方按 desc_.id 解析
case geopro::data::OpKind::Filter:
acts.push_back({"filter", "筛选",
[this]{ filterRow_->setVisible(!filterRow_->isVisible()); }, {}});
break;
case geopro::data::OpKind::PlaneZ:
acts.push_back({"layers", "z值", {},
[this](QToolButton* b){ /* Task E3 建滑块 popup */ emit zSliderRequested(QString::fromStdString(desc_.id)); }});
break;
case geopro::data::OpKind::Basemap:
acts.push_back({"map", "底图", {},
[this](QToolButton* b){ /* Task F2 建底图 popup */ emit basemapPopupRequested(QString::fromStdString(desc_.id)); }});
break;
}
}
iconBar_->setActions(acts);
hl->addWidget(iconBar_);
```
glyph 键 `"plus"/"filter"/"layers"/"map"` 映射到 `Glyphs.hpp` 现有键,缺则补近义键。`generateVolumeRequested` 第一参数原为 dsTypeCode——改由接收方按 `desc_.id` 经 catalog 反查,或保留 `desc_` 内冗余存一个 dsTypeCode 字段;本计划取「接收方按 id 解析」。)
- [ ] **Step 3: hpp 增成员/信号**`CategorySection.hpp``SectionIconBar* iconBar_=nullptr; QWidget* filterRow_=nullptr;`;新增信号 `void zSliderRequested(); void basemapPopupRequested();`Phase E/F 用)。移除 `radarImportRequested`Phase D 删其发射)。
- [ ] **Step 2: `FilterKind→UI` 建筛选行(默认折叠)** — 替换 `CategorySection.cpp:119-132`:建 `QWidget* filterRow_`(默认 `hide()`),遍历 `desc_.filters``DateRange`→建 `DateRangeEdit``ArrayType`→建 `arrayCombo_`。`passesFilters`/`rebuildList` 据建出的控件判断(无该控件即不筛该维度)。
- [ ] **Step 3: hpp 改造**`CategorySection.hpp`:构造签名改 `const geopro::data::CategoryDescriptor&`;成员 `desc_`;加 `SectionIconBar* iconBar_=nullptr; QWidget* filterRow_=nullptr;`;信号加 `zSliderRequested(QString)`/`basemapPopupRequested(QString)`;移除 `radarImportRequested`。`CategoryAnalysisTab.cpp` 构造改 `for (const auto& desc : geopro::data::categoryCatalog()) new CategorySection(desc, dict, content);``sections_[desc.id]`。
- [ ] **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): 段头改图标工具条 + 筛选默认折叠 + 新增三维体图标化"
git add src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp src/app/panels/columns/CategoryAnalysisTab.cpp
git commit -m "feat(vtk): 段构造迁描述符;段头图标条由operations/filters驱动+筛选折叠"
```
---
@ -608,15 +858,15 @@ git commit -m "feat(vtk): 3D 底图控件移渲染区工具栏(天地图/无+透
**Files:**
- Modify: `src/app/VtkSceneView.hpp/.cpp:420-439``setAnalysisMode2D`)、`482-539``pickMapLineAt`/`nudgeSelectedMapLinesZ`
- Modify: `src/controller/VtkSceneController.*``onAnalysisModeChanged`、`view2DMode`/`set2DPlacement` mode 维度
- Modify: `src/controller/VtkSceneController.*`残留的 `onAnalysisModeChanged`/`view2DMode`/`placement2dMode_``set2DPlacement` 公有入口已在 B2 随策略切换删除,此处清剩余私有状态
- Modify: `src/app/main.cpp`(拖 Z 浮层 `498-505`、`544-557`
**Interfaces:**
- Produces: 场景恒自由透视;删除 `setAnalysisMode2D`、`nudgeSelectedMapLinesZ`、`mapLineZOffset_`、拖 Z 浮层。`set2DPlacement(mode,z)` 简化为后续平面 z 接口Task E2 替换)
- Produces: 场景恒自由透视;删除 `setAnalysisMode2D`、`nudgeSelectedMapLinesZ`、`mapLineZOffset_`、拖 Z 浮层、残留 `view2DMode`/`placement2dMode_` 状态
- [ ] **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 2: 删 controller analysisMode/view2DMode 残留** — 移除 `onAnalysisModeChanged`、`placement2dMode_`/`customZ2d_`/`placementZ()`/`view2DMode` 等残留状态(其渲染职责已在 B2 迁入 `Plane2DRenderStrategy`
- [ ] **Step 3: 删 main 拖 Z 浮层接线** — 移除 `main.cpp:498-505`(高程读数浮层)、`544-557`pick/nudge 接线)。
@ -629,27 +879,27 @@ git add src/app/VtkSceneView.hpp src/app/VtkSceneView.cpp src/controller/VtkScen
git commit -m "refactor(vtk): 删维度相机/显隐耦合与逐ds拖Z(单一自由场景)"
```
### Task E2: 2D 按类型平面 z 生命周期(纯逻辑 TDD+ 控制器接入
### Task E2: Plane2DRenderStrategy 装入平面 z 生命周期PlaneZRegistry纯逻辑 TDD
**Files:**
- Create: `src/controller/PlaneBasemapManager.hpp/.cpp`(先只含平面 z 生命周期逻辑;底图实例 Phase F 加
- Create: `tests/controller/test_plane_basemap_manager.cpp`
- Create: `src/controller/PlaneZRegistry.hpp`(纯逻辑头,平面 z 生命周期
- Create: `tests/controller/test_plane_z_registry.cpp`
- Modify: `tests/CMakeLists.txt`、`CMakeLists.txt`
- Modify: `src/controller/VtkSceneController.cpp`2D 落点改用管理器平面 z
- Modify: `src/controller/DatasetRenderStrategy.hpp/.cpp``Plane2DRenderStrategy` 持 `PlaneZRegistry``add/remove/onTypeDeactivated` 用之
**Interfaces:**
- Produces: `class PlaneZRegistry`(纯逻辑核):
- Produces: `class PlaneZRegistry`(纯逻辑核,被 `Plane2DRenderStrategy` 持有
- `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`
- [ ] **Step 1: 写失败测试**`tests/controller/test_plane_z_registry.cpp`
```cpp
#include <gtest/gtest.h>
#include "controller/PlaneBasemapManager.hpp"
#include "controller/PlaneZRegistry.hpp"
using geopro::controller::PlaneZRegistry;
@ -689,7 +939,7 @@ TEST(PlaneZRegistry, SetPlaneZMovesPlane) {
- [ ] **Step 2: 跑测试确认失败**`build.bat test`;预期编译失败。
- [ ] **Step 3: 实现 `PlaneBasemapManager.hpp` 的 `PlaneZRegistry`**
- [ ] **Step 3: 实现 `PlaneZRegistry.hpp`**
```cpp
#pragma once
@ -730,21 +980,24 @@ private:
} // 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 5: 登记 CMake**`tests/CMakeLists.txt` 加 `test_plane_z_registry.cpp``PlaneZRegistry.hpp` 为纯头,无需加源)
- [ ] **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 所属段 id2D 类型),由 ddCode 映射(`dd_trajectory_data`→"trajectory")。
- [ ] **Step 6: `Plane2DRenderStrategy` 装入 `PlaneZRegistry`** — 让 A2 建的 `Plane2DRenderStrategy``PlaneZRegistry planeReg_;`,并把 Phase B 里它「转调 `add2DDatasetAsync`」的实现改为:
- `add(typeId, dsId)``dsZ` = 该 ds 既有 z无则取场景地表基准 `view_.zRefElev()`)→ `double z = planeReg_.onChecked(typeId, dsId, dsZ)``repo_.loadMapLine(dsId, ...)` 落地时 `view_.addMapLine(dsId, line, z)`z 取 `planeReg_.planeZ(typeId)` 当前值)。
- `remove(dsId)``view_.removeDataset(dsId)`;并 `planeReg_.onUnchecked(typeId, dsId)`typeId 由 strategy 持有的 dsId→typeId 小表或随 remove 传参得到——`IDatasetRenderStrategy::remove` 只有 dsId故 strategy 内自存 `dsId→typeId`)。
- `onTypeDeactivated(typeId)`:该类型全消(`PlaneZRegistry` 此时已 `hasPlane==false`)→ Phase F 在此销毁平面底图。
- `typeId` 即描述符 id控制器 `setCheckedDatasets` 传入§B2
- [ ] **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/全消消失)+单元测试"
git add src/controller/PlaneZRegistry.hpp tests/controller/test_plane_z_registry.cpp tests/CMakeLists.txt src/controller/DatasetRenderStrategy.hpp src/controller/DatasetRenderStrategy.cpp
git commit -m "feat(vtk): Plane2D 策略装入平面 z 生命周期(首勾定z/全消消失)+单元测试"
```
### Task E3: 段「z值」滑块 popup 接平面 z
@ -754,13 +1007,13 @@ git commit -m "feat(vtk): 2D 按类型平面 z 生命周期(首勾定z/全消消
- 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` + 重摆该类型足迹。
- Produces: `CategorySection` 信号 `void planeZChanged(const QString& typeId, double z);`controller `void setPlaneZ(const QString& typeId, double z)`(转交 `Plane2DRenderStrategy::setPlaneZ`)。`Plane2DRenderStrategy::setPlaneZ(typeId,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 1: z值 popup** — Task C2 的 z值 action `popupBuilder` 内建 `QSlider`(范围按场景高程量级,如 -500..500 米,默认取当前 `planeZ``valueChanged` → `emit planeZChanged(typeIdOfSpec, v)`。`typeId` = `descriptor.id`。
- [ ] **Step 2: 转发链**`CategoryAnalysisTab` 转发 `planeZChanged`main 接到 → `sceneCtrl->setPlaneZ(typeId, z)`
- [ ] **Step 3: controller setPlaneZ** — `planeReg_.setPlaneZ(typeId,z)` 后,对该类型已勾选足迹 `removeDataset`+`addMapLine(...,z)` 重摆(参照旧 `set2DPlacement` 重摆法)。
- [ ] **Step 3: 落到策略** — `VtkSceneController::setPlaneZ``Plane2DRenderStrategy``registry_.get("plane2d")` 下转型,或控制器持其指针)调 `setPlaneZ(typeId,z)`:内部 `planeReg_.setPlaneZ(typeId,z)` 后,对该类型已勾选足迹 `removeDataset`+`addMapLine(...,z)` 重摆(参照旧 `set2DPlacement` 重摆法)。
- [ ] **Step 4: 构建 + 手动验收**`build.bat app`。验收:拖 z值滑块 → 该类型整块平面(含其上全部足迹)整体升降。
@ -796,29 +1049,29 @@ git add src/app/TileBasemap.hpp src/app/TileBasemap.cpp
git commit -m "refactor(vtk): TileBasemap groundZ 参数化支持多实例平面底图"
```
### Task F2: PlaneBasemapManager 管 N 个平面底图实例 + 段「底图」popup
### Task F2: Plane2DRenderStrategy 持 N 个平面底图 + 段「底图」popup
**Files:**
- Modify: `src/controller/PlaneBasemapManager.hpp/.cpp`加底图实例管理)
- Modify: `src/controller/VtkSceneController.*`、`src/app/main.cpp`、`CategorySection.*`
- Modify: `src/controller/DatasetRenderStrategy.hpp/.cpp``Plane2DRenderStrategy` 加底图实例管理)
- Modify: `src/controller/VtkSceneController.*`、`src/app/main.cpp`、`CategorySection.*`、`CategoryAnalysisTab.*`
**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
- Consumes: `PlaneZRegistry`已在 strategy 内)、`TileBasemap`多实例F1)、`Scene&`/`vtkRenderWindow*`/`GeoLocalFrame`、`dataRadiusProvider`strategy 构造时注入)
- Produces: `Plane2DRenderStrategy` 内 `std::map<std::string, std::unique_ptr<TileBasemap>> bms_;`(按 typeId新方法 `setBasemapKind(typeId,kind)` / `setBasemapOpacity(typeId,o)``onTypeActivated/Deactivated` 建/销毁底图;`setPlaneZ` 同步重建底图于新 z。controller 转发 `setBasemapKind/Opacity`
- [ ] **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 1: strategy 持底图实例** — `Plane2DRenderStrategy``std::map<std::string, std::unique_ptr<TileBasemap>> bms_;`。`onTypeActivated(typeId)`(该类型首勾,平面已建):`bms_[typeId] = std::make_unique<TileBasemap>(scene_, rw_, frame_, planeReg_.planeZ(typeId))` → `setDataRadiusProvider(provider_)``show(Street)`(纯平矢量)+ `setOpacity(0.5)`。`onTypeDeactivated(typeId)`(全消):`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 2: 段「底图」popup** — Task C2 的底图 action `popupBuilder`:菜单【矢量平面(默认)/无】+ 透明度滑块(默认50)`CategorySection` 发 `basemapKindChanged(typeId,kind)`/`basemapOpacityChanged(typeId,o)` → `CategoryAnalysisTab` 转发 → main → `sceneCtrl->setBasemapKind/Opacity(typeId,...)``Plane2DRenderStrategy`。「无」= `bms_[typeId]->hide()`(保留实例,仅隐瓦片)
- [ ] **Step 3: 段「底图」popup** — Task C2 的底图 action `popupBuilder`:菜单【矢量平面(默认)/无】+ 透明度滑块(默认50);发 `basemapKindChanged(typeId,kind)`/`basemapOpacityChanged(typeId,o)` → main → `mgr.setKind/ setOpacity`。「无」= `bms_[typeId]->hide()`(保留实例,仅隐瓦片)
- [ ] **Step 3: 构建 + 手动验收** — `build.bat app`。验收:勾首条轨迹 → 出现该类型平面矢量底图(贴平面 z、纯平无高程、范围≈数据范围z值滑块带底图同升降底图 popup 可切无/调透明度;全取消 → 平面+底图一并消失
- [ ] **Step 4: 构建 + 手动验收** — `build.bat app`。验收:勾首条轨迹 → 出现该类型平面矢量底图(贴平面 z、纯平无高程、范围≈数据范围z值滑块带底图同升降底图 popup 可切无/调透明度;全取消 → 平面+底图一并消失
- [ ] **Step 4: 退役旧分类件** — 确认 `splitByDimension`/`DatasetDimension.*`、`CategoryConfig.hpp` 无引用后删除(`grep -rn` 核对);`CMakeLists.txt` 去登记
- [ ] **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)"
git add src/controller/DatasetRenderStrategy.hpp src/controller/DatasetRenderStrategy.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): Plane2D 策略持每类型平面矢量底图(多实例+生命周期+底图popup)"
```
---
@ -826,16 +1079,20 @@ git commit -m "feat(vtk): 2D 每类型平面矢量底图(TileBasemap多实例+
## Self-Review计划对照 spec
**Spec coverage**
- §5 类型抽象(描述符目录 + classify 谓词 + 扩展契约)→ Task A1 ✓;渲染策略接口/注册表/3 策略 → Task A2 ✓
- §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 ✓
- §5.3 消费方只认抽象(勾选经描述符路由策略,无维度散判)→ B2
- §6 图标条`operations(OpKind)` 驱动 +「…」溢出(数量+宽度两分支) → Task C1/C2 ✓
- §7.1 筛选(`Filter`,由 `filters` 驱动) → C2§7.2 新增三维体(`GenerateVolume`) → C2§7.3 z值(`PlaneZ`) → E3§7.4 2D底图(`Basemap`) → F2§7.5 3D底图移工具栏 → D2/D3§7.6 导入雷达移设备菜单 → D1 ✓
- §8 单一自由场景 + 经策略渲染 + 2D按类型平面(`Plane2DRenderStrategy`) + 废弃逐ds拖Z/view2DMode → E1/E2/E3 ✓
- §9.1 3D共享底图(透明度可调/隐藏) → D2/D3§9.2 N个2D平面底图(TileBasemap多实例,封装于 Plane2D 策略) → F1/F2 ✓
- §10 默认勾选保留 + 2D信号迁段 → B2/E2/E3 ✓
- §12.10 可扩展性(catalog+策略驱动、加描述符即接入) → A1/A2 + 全程消费方改造 ✓
**Placeholder scan** UI 接线步骤C1-Step7、F2 等)给出真实代码骨架 + 参照锚点(`VtkViewToolbar::iconBtn`、`Glyphs.hpp`、旧 `set2DPlacement` 重摆法),非 TODO。纯逻辑任务A1/C1/E2)含完整测试与实现代码。
**Placeholder scan** UI 接线步骤给出真实代码骨架 + 参照锚点(`VtkViewToolbar::iconBtn`、`Glyphs.hpp`、旧 `set2DPlacement` 重摆法),非 TODO。纯逻辑任务A1 描述符/分流、A2 注册表、C1 图标溢出、E2 平面 z)含完整测试与实现代码。
**Type consistency** `CategoryDim`(A1) / `visibleIconCount`(C1) / `PlaneZRegistry`(E2) / `TileBasemap::setOpacity`(D2)·`groundZ`(F1) / `setChecked3DDatasets`·`setChecked2DDatasets`·`setPlaneZ`(B2/E2/E3) 跨任务名称一致。
**Type consistency** `CategoryDescriptor`/`categoryCatalog`/`SceneKind`/`FilterKind`/`OpKind`/`byDdCode`/`byDsTypeCode`(A1) / `IDatasetRenderStrategy`/`RenderStrategyRegistry`/`VolumeRenderStrategy`/`CurtainRenderStrategy`/`Plane2DRenderStrategy`/策略键 `"volume"/"curtain"/"plane2d"`(A2) / `setCheckedDatasets(dsId→typeId)`(B2) / `visibleIconCount`(C1) / `PlaneZRegistry`(E2,置 `Plane2DRenderStrategy` 内) / `setPlaneZ`/`setBasemapKind`/`setBasemapOpacity`(E3/F2) / `TileBasemap::setOpacity`(D2)·`groundZ`(F1) 跨任务名称一致。
**已知待实现期细化(非占位,属合理实现细节):** z值滑块数值范围E3按场景高程量级`Glyph::Map` 若不存在选近义键D3`buildDeviceMenu` 自由函数 vs 成员法D1按实际定义形态
**过渡策略:** A1「加新不删旧」——`CategoryDescriptor`/`categoryCatalog` 与旧 `CategorySpec`/`categoryConfigs` 同 id 同序并存,`splitByCategory` 先切到 catalogPhase C 迁 `CategorySection`/`CategoryAnalysisTab` 到描述符后、Phase F Step4 删旧 `CategoryConfig.hpp`/`DatasetDimension.*`。全程可编译。
**已知待实现期细化(非占位,属合理实现细节):** z值滑块数值范围E3按场景高程量级`Glyph::Map` 若不存在选近义键D3`buildDeviceMenu` 自由函数 vs 成员法D1按实际定义形态`Plane2DRenderStrategy` 构造所需 `Scene&/rw/frame/repo/view` 引用的具体注入形态A2/E2`VtkSceneController` 现有持有)。