# 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:** 把 VTK 工作台的"旧二维/三维切换 + 三浮层"过渡态,重构成需求 A1 的「三个子列表栏」(三维数据集 / 二维数据集 / 三维分析),内嵌在唯一的中央「VTK视图」左侧,并接通已有渲染/切片能力;同时给 VTK视图 + 数据详情 加全屏按钮。 **Architecture:** 三栏抽成独立 widget(`src/app/panels/columns/`),各自只发信号、不依赖控制器;`ColumnDrawer` 作为 `vtkWidget` 在 HBox 中的**左侧兄弟控件**(非 GL 浮层,规避原生 GL 浮层 z 序/圆角伪影),可折叠。`main.cpp::buildWorkbench` 删三浮层+分段切换,改挂三栏并把信号接到既有 `VtkSceneController`/`InteractionManager`/`DatasetDetailController`。数据集列表由 `WorkbenchNavController` 取 `DsRow`、按 `I3dSceneRepository::dimensionOf` 过滤后分发到三栏。 **Tech Stack:** C++17, Qt6 Widgets, VTK9, Qt-ADS。构建 `build.bat`(见"构建/验证铁律"),测试 GoogleTest/CTest。 --- ## ⚠️ 构建/验证铁律(每个 Task 都遵守) - 构建:从 Git Bash 调 `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat test"`。命令:`app`/`test`/`rebuild`(全量)/`configure`。 - **ninja 偶发漏编** → 改头/布局后用 `build.bat rebuild`;验 exe 新鲜:`stat -c '%y' build/release/src/app/geopro_desktop.exe`。 - **切勿 `rm -rf build/release`**(vcpkg 重编依赖极慢)。 - **Claude 工具跑 build 偶被 Start-Process 钩子劫持静默不跑** → **所有"用户实测"步骤必须由用户在其终端 `build.bat rebuild` 跑并目视**。Claude 不能 GUI 测 VTK 交互。 - 纯逻辑(如维度过滤)抽函数 + GoogleTest 单测;UI/交互靠 build 绿 + 用户实测清单。 ## 测试方式约定(本计划特例,覆盖默认 TDD-everywhere) - **逻辑步骤**(标 `[逻辑]`):先写失败测试 → 跑红 → 实现 → 跑绿 → 提交。 - **UI 步骤**(标 `[UI]`):实现 → `build.bat rebuild` 编绿 → **用户实测清单** → 提交。Claude 不声称"已验证"交互,只验证编译通过 + 代码读校。 --- ## File Structure **新建:** - `src/app/panels/columns/ColumnDrawer.hpp/.cpp` — 抽屉容器(QTabWidget 三 tab + 折叠开关)。 - `src/app/panels/columns/Column3DDataset.hpp/.cpp` — 三维数据集栏(4 工具条栏位 + 3D 数据集树)。 - `src/app/panels/columns/Column2DDataset.hpp/.cpp` — 二维数据集栏(地图/2D视图控件 + 2D 数据集树)。 - `src/app/panels/columns/Column3DAnalysis.hpp/.cpp` — 三维分析栏(对象→三维体→切片 树 + 两个右键菜单)。 - `src/app/DatasetDimension.hpp/.cpp` — 纯函数 `splitByDimension(...)`(可单测)。 - `tests/app/test_dataset_dimension.cpp` — 维度过滤单测。 **修改:** - `src/app/main.cpp` — `buildWorkbench`:删三浮层(393-556)/分段切换(380-389,832-843)/showLayerPanel(804-827)/相关 connect;改挂 ColumnDrawer;接信号;rename vtkDock;bump dockState 版本;接维度过滤;全屏按钮。 - `src/app/Glyphs.hpp/.cpp` — 加 `Glyph::Fullscreen` + SVG。 - `src/app/CMakeLists.txt` — 加新源文件。 - `tests/CMakeLists.txt`(或对应)— 加 test_dataset_dimension。 --- ## Task 1: 加 Fullscreen 图标 [UI] **Files:** - Modify: `src/app/Glyphs.hpp:15-35`(Glyph 枚举) - Modify: `src/app/Glyphs.cpp`(SVG path 映射,参照现有 case) - [ ] **Step 1: 在 Glyph 枚举加 Fullscreen** `src/app/Glyphs.hpp`,在 `Collapse,` 之后加: ```cpp Collapse, // 折叠(双箭头) Fullscreen, // 全屏 / 最大化 ``` - [ ] **Step 2: 在 Glyphs.cpp 的 svg 映射加 case** 找到 `makeGlyph`/svg path 的 `switch`(参照 `case Glyph::Collapse:`),加: ```cpp case Glyph::Fullscreen: return QStringLiteral(""); ``` (若已有 restore/缩小语义图标可复用,但需一个独立 Fullscreen 项。) - [ ] **Step 3: 编译** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat app"` Expected: 编译通过(exe 重新生成)。 - [ ] **Step 4: 提交** ```bash git add src/app/Glyphs.hpp src/app/Glyphs.cpp git commit -m "feat(vtk): 加 Glyph::Fullscreen 图标(三栏重构全屏按钮用)" ``` --- ## Task 2: 维度过滤纯函数 [逻辑] 把"DsRow 列表 → 按维度分三组"抽成可单测纯函数。`dimensionOf` 已在 `I3dSceneRepository`(接口),这里做的是**列表分流**。 **Files:** - Create: `src/app/DatasetDimension.hpp` - Create: `src/app/DatasetDimension.cpp` - Test: `tests/app/test_dataset_dimension.cpp` - Modify: `src/app/CMakeLists.txt`、`tests/CMakeLists.txt` - [ ] **Step 1: 写失败测试** `tests/app/test_dataset_dimension.cpp`: ```cpp #include #include "app/DatasetDimension.hpp" #include "data/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("c", "dd_trajectory_data"), // 2D row("d", "dd_slice"), // Analysis row("e", "dd_unknownxyz"), // Other → 不入任何栏 }; DimBuckets b = splitByDimension(in); ASSERT_EQ(b.dim3D.size(), 2u); EXPECT_EQ(b.dim3D[0].id, "a"); EXPECT_EQ(b.dim3D[1].id, "b"); 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()); } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat test"` Expected: FAIL(`DatasetDimension.hpp` 不存在 / 链接失败)。 - [ ] **Step 3: 写实现** `src/app/DatasetDimension.hpp`: ```cpp #pragma once #include #include "data/repo/RepoTypes.hpp" namespace geopro::app { struct DimBuckets { std::vector dim3D; std::vector dim2D; std::vector analysis; }; // 按 ddCode 把 ds 分流到 三维数据集 / 二维数据集 / 三维分析 三栏。 // Other 维度不入任何栏(保留 parentId 顺序,调用方可直接喂 populateDatasetList)。 DimBuckets splitByDimension(const std::vector& rows); } // namespace geopro::app ``` `src/app/DatasetDimension.cpp`: ```cpp #include "app/DatasetDimension.hpp" namespace geopro::app { namespace { // 与 LocalSample3dRepository::dimensionOf 同一映射(spec §6.1)。 // 抽到此处以便纯函数单测;将来后端返 dimension 字段时此函数改读字段即可。 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") 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 ``` > 注:`dimensionOf` 同时存在于 `LocalSample3dRepository`(渲染编排用)。此处复制映射是**有意**——纯函数便于单测、且与"将来后端返 dimension 字段"解耦。后续若收敛为单一真源,再让本函数调用注入的 repo。落地时若 reviewer 要求单一真源,可改签名 `splitByDimension(rows, const I3dSceneRepository&)`,本期按纯函数。 - [ ] **Step 4: 注册到 CMake** `src/app/CMakeLists.txt`:把 `DatasetDimension.cpp` 加入 app 目标源列表(仿照同目录 .cpp 的加法)。 `tests/CMakeLists.txt`(或 tests/app):把 `test_dataset_dimension.cpp` 加入测试目标(仿照 `test_3d_repo` 的注册)。 - [ ] **Step 5: 跑测试确认通过** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat test"` Expected: `DatasetDimension.*` 2 项 PASS;总数 ≥ 223/223。 - [ ] **Step 6: 提交** ```bash git add src/app/DatasetDimension.hpp src/app/DatasetDimension.cpp tests/app/test_dataset_dimension.cpp src/app/CMakeLists.txt tests/CMakeLists.txt git commit -m "feat(vtk): 维度过滤纯函数 splitByDimension + 单测" ``` --- ## Task 3: 三维数据集栏 widget [UI] 独立 widget:4 工具条栏位(坐标轴设置 / 水平垂直比例 / 快捷视图 / 缩放)+ 数据集树。只发信号。控件创建可**搬运** `main.cpp:433-516`(axisBar)的 combo/slider/button 构造与样式,重排成 4 分组(参照原型 `docs/superpowers/mockups/2026-06-16-three-column-layout.html` 的 `toolbar3D`)。 **Files:** - Create: `src/app/panels/columns/Column3DDataset.hpp/.cpp` - Modify: `src/app/CMakeLists.txt` - [ ] **Step 1: 写头文件(信号 API)** `src/app/panels/columns/Column3DDataset.hpp`: ```cpp #pragma once #include #include #include #include "controller/I3dSceneView.hpp" // AxesMode/AxesUnit/ViewDir #include "data/repo/RepoTypes.hpp" class QTreeWidget; namespace geopro::app { // 三维数据集栏:坐标轴设置 + 水平/垂直比例 + 快捷视图 + 缩放 + 3D 数据集列表。 class Column3DDataset : public QWidget { Q_OBJECT public: explicit Column3DDataset(QWidget* parent = nullptr); // 用 3D 维度的 ds 填充列表(调用 populateDatasetList)。 void setDatasets(const std::vector& rows); signals: void axesModeChanged(geopro::controller::AxesMode mode); void axesUnitChanged(geopro::controller::AxesUnit unit); void verticalExaggerationChanged(double ve); void viewRequested(geopro::controller::ViewDir dir); void zoomInRequested(); void zoomOutRequested(); void fitRequested(); void oPointClicked(); // O点位置按钮(本期弹框留 stub) void fontClicked(); // 字体按钮(本期 stub) void checkedDatasetsChanged(const QStringList& dsIds); // 列表勾选变化 private: QTreeWidget* list_ = nullptr; }; } // namespace geopro::app ``` - [ ] **Step 2: 写实现(构造 4 分组 + 列表)** `src/app/panels/columns/Column3DDataset.cpp`:用 `QVBoxLayout` 堆 4 个分组 `QGroupBox`/`QFrame`(标题 + 表单行)+ `QTreeWidget` 列表。控件构造照搬 `main.cpp:464-500`(axesModeCombo/axesUnitCombo/veSlider/btnFront..btnFit),样式用 `applyTokenizedStyleSheet` 照搬 `main.cpp:437-457`。各控件 `connect` 到本类 `emit ...`。要点(完整骨架): ```cpp #include "app/panels/columns/Column3DDataset.hpp" #include #include #include #include #include #include #include #include #include "app/Theme.hpp" #include "app/panels/DatasetListPanel.hpp" // populateDatasetList using geopro::controller::AxesMode; using geopro::controller::AxesUnit; using geopro::controller::ViewDir; namespace geopro::app { Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) { auto* root = new QVBoxLayout(this); root->setContentsMargins(space::kMd, space::kMd, space::kMd, space::kMd); root->setSpacing(space::kMd); // —— 坐标轴设置 组:显示方式▾ / O点位置(按钮) / 刻度▾ / 字体(按钮) —— { auto* form = new QFormLayout(); auto* mode = new QComboBox(); mode->addItem(QStringLiteral("标准"), static_cast(AxesMode::Standard)); mode->addItem(QStringLiteral("三维立体"), static_cast(AxesMode::Stereo)); mode->addItem(QStringLiteral("不显示"), static_cast(AxesMode::None)); connect(mode, qOverload(&QComboBox::currentIndexChanged), this, [this, mode](int){ emit axesModeChanged(static_cast(mode->currentData().toInt())); }); auto* oPoint = new QPushButton(QStringLiteral("设置…")); connect(oPoint, &QPushButton::clicked, this, &Column3DDataset::oPointClicked); auto* unit = new QComboBox(); unit->addItem(QStringLiteral("无刻度"), static_cast(AxesUnit::None)); unit->addItem(QStringLiteral("米"), static_cast(AxesUnit::Meter)); unit->addItem(QStringLiteral("英尺"), static_cast(AxesUnit::Feet)); unit->addItem(QStringLiteral("经纬度"), static_cast(AxesUnit::LatLon)); unit->setCurrentIndex(1); connect(unit, qOverload(&QComboBox::currentIndexChanged), this, [this, unit](int){ emit axesUnitChanged(static_cast(unit->currentData().toInt())); }); auto* font = new QPushButton(QStringLiteral("设置…")); connect(font, &QPushButton::clicked, this, &Column3DDataset::fontClicked); form->addRow(QStringLiteral("显示方式"), mode); form->addRow(QStringLiteral("O点位置"), oPoint); form->addRow(QStringLiteral("刻度"), unit); form->addRow(QStringLiteral("字体"), font); root->addWidget(new QLabel(QStringLiteral("坐标轴设置"))); root->addLayout(form); } // —— 水平/垂直比例 组:单个滑块 + 数值 —— { auto* row = new QHBoxLayout(); auto* slider = new QSlider(Qt::Horizontal); slider->setMinimum(1); slider->setMaximum(10); slider->setValue(2); auto* val = new QLabel(QStringLiteral("2.0×")); connect(slider, &QSlider::valueChanged, this, [this, val](int v){ val->setText(QStringLiteral("%1.0×").arg(v)); emit verticalExaggerationChanged(static_cast(v)); }); row->addWidget(slider, 1); row->addWidget(val); root->addWidget(new QLabel(QStringLiteral("水平/垂直比例"))); root->addLayout(row); } // —— 快捷视图 组:前/后/左/右/上/下 —— { auto* row = new QHBoxLayout(); struct V { const char* t; ViewDir d; }; for (V v : { V{"前",ViewDir::Front}, V{"后",ViewDir::Back}, V{"左",ViewDir::Left}, V{"右",ViewDir::Right}, V{"上",ViewDir::Top}, V{"下",ViewDir::Bottom} }) { auto* b = new QPushButton(QString::fromUtf8(v.t)); ViewDir d = v.d; connect(b, &QPushButton::clicked, this, [this, d]{ emit viewRequested(d); }); row->addWidget(b); } root->addWidget(new QLabel(QStringLiteral("快捷视图"))); root->addLayout(row); } // —— 缩放 组:放大/缩小/适配 —— { auto* row = new QHBoxLayout(); auto* in = new QPushButton(QStringLiteral("放大")); auto* out = new QPushButton(QStringLiteral("缩小")); auto* fit = new QPushButton(QStringLiteral("适配")); connect(in, &QPushButton::clicked, this, &Column3DDataset::zoomInRequested); connect(out, &QPushButton::clicked, this, &Column3DDataset::zoomOutRequested); connect(fit, &QPushButton::clicked, this, &Column3DDataset::fitRequested); row->addWidget(in); row->addWidget(out); row->addWidget(fit); root->addWidget(new QLabel(QStringLiteral("缩放"))); root->addLayout(row); } // —— 数据集列表(3D 维度)—— list_ = new QTreeWidget(); list_->setHeaderHidden(true); list_->setRootIsDecorated(true); applyDatasetCardDelegate(list_); connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int){ QStringList ids; for (QTreeWidgetItemIterator it(list_); *it; ++it) if ((*it)->checkState(0) == Qt::Checked) ids << (*it)->data(0, /*kDsIdRole*/ Qt::UserRole + 1).toString(); emit checkedDatasetsChanged(ids); }); root->addWidget(list_, 1); } void Column3DDataset::setDatasets(const std::vector& rows) { populateDatasetList(list_, rows, /*append=*/false); // 列表项需可勾选:populateDatasetList 后给每项加 Qt::ItemIsUserCheckable + Unchecked。 for (QTreeWidgetItemIterator it(list_); *it; ++it) { (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable); if ((*it)->checkState(0) == Qt::Unchecked || (*it)->checkState(0) == Qt::Checked) {} else (*it)->setCheckState(0, Qt::Unchecked); } } } // namespace geopro::app ``` > 注 1:`kDsIdRole` 的真实值见 `src/app/panels/DatasetListPanel.cpp`(`Qt::UserRole+? `)。落地时 include 该常量或用其公开定义,勿硬编码 `UserRole+1`——读 DatasetListPanel.hpp/.cpp 取真实 role 常量。 > 注 2:`populateDatasetList` 生成的项默认不可勾选;本栏需勾选渲染,故 setDatasets 后补 `ItemIsUserCheckable` + `Unchecked`(见上)。 > 注 3:分组标题/表单样式照原型;可用 `applyTokenizedStyleSheet` 套深色令牌(搬 `main.cpp:437-457`)。 - [ ] **Step 3: 注册 CMake + 编译** `src/app/CMakeLists.txt` 加 `panels/columns/Column3DDataset.cpp`。 Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat app"` Expected: 编译通过。 - [ ] **Step 4: 提交** ```bash git add src/app/panels/columns/Column3DDataset.hpp src/app/panels/columns/Column3DDataset.cpp src/app/CMakeLists.txt git commit -m "feat(vtk): 三维数据集栏 widget(4工具条栏位+3D数据集列表,只发信号)" ``` --- ## Task 4: 二维数据集栏 widget [UI] **Files:** - Create: `src/app/panels/columns/Column2DDataset.hpp/.cpp` - Modify: `src/app/CMakeLists.txt` - [ ] **Step 1: 写头文件** `Column2DDataset.hpp`: ```cpp #pragma once #include #include #include #include "data/repo/RepoTypes.hpp" class QTreeWidget; namespace geopro::app { class Column2DDataset : public QWidget { Q_OBJECT public: explicit Column2DDataset(QWidget* parent = nullptr); void setDatasets(const std::vector& rows); signals: void basemapChanged(int index); // 0 天地图 / 1 Google / 2 隐藏 void view2DModeChanged(int index); // 0 关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义 void customZChanged(double z); // 世界绝对高程(米),向上为正 void checkedDatasetsChanged(const QStringList& dsIds); private: QTreeWidget* list_ = nullptr; }; } ``` - [ ] **Step 2: 写实现** `Column2DDataset.cpp`:地图 combo(天地图/Google Map/隐藏)→ `basemapChanged`;2D视图 combo(关闭/Z=0/顶部/底部/自定义)→ `view2DModeChanged`,选"自定义"时显一个 `QDoubleSpinBox`(范围 ±1e6,后缀 " m")→ `customZChanged`;`QTreeWidget` 列表同 Task3(可勾选 + setDatasets 用 populateDatasetList)。骨架同 Column3DDataset 模式(QFormLayout 两组 + 列表),此处不赘述控件 connect(与 Task3 同形)。自定义 Z 输入框默认 `setVisible(false)`,在 `view2DModeChanged` 槽里 `zEdit->setVisible(index==4)`。 - [ ] **Step 3: 注册 CMake + 编译** `src/app/CMakeLists.txt` 加 `panels/columns/Column2DDataset.cpp`。 Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat app"` Expected: 编译通过。 - [ ] **Step 4: 提交** ```bash git add src/app/panels/columns/Column2DDataset.hpp src/app/panels/columns/Column2DDataset.cpp src/app/CMakeLists.txt git commit -m "feat(vtk): 二维数据集栏 widget(地图/2D视图+自定义Z输入+2D列表)" ``` --- ## Task 5: 三维分析栏 widget + 两个右键菜单 [UI] **Files:** - Create: `src/app/panels/columns/Column3DAnalysis.hpp/.cpp` - Modify: `src/app/CMakeLists.txt` - [ ] **Step 1: 写头文件** `Column3DAnalysis.hpp`: ```cpp #pragma once #include #include #include #include "data/repo/RepoTypes.hpp" #include "render/interact/SlicePlaneMath.hpp" // SliceAxis class QTreeWidget; class QTreeWidgetItem; namespace geopro::app { // 三维分析栏:对象→三维体模型→切片 树 + 三维体/切片两套右键菜单。 class Column3DAnalysis : public QWidget { Q_OBJECT public: explicit Column3DAnalysis(QWidget* parent = nullptr); void setDatasets(const std::vector& rows); // Analysis 维度(三维体/切片) signals: // 三维体右键:切片▸(上下/前后/左右/任意) void sliceRequested(geopro::render::interact::SliceAxis axis); void colorScaleRequested(const QString& dsId); // 三维体&切片(本期 stub) void visibilityToggled(const QString& dsId); // 显示/隐藏 void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 切片右键(本期 stub,菜单可见但发信号给上层提示"待实现") void sliceSaveRequested(const QString& dsId); void sliceSaveAsRequested(const QString& dsId); void sliceExportRequested(const QString& dsId); void sliceDeleteRequested(const QString& dsId); void checkedItemsChanged(const QStringList& dsIds); private: void onContextMenu(const QPoint& pos); QTreeWidget* tree_ = nullptr; }; } ``` - [ ] **Step 2: 写实现(树 + 右键分派)** `Column3DAnalysis.cpp`:`tree_` 设 `setContextMenuPolicy(Qt::CustomContextMenu)`,connect `customContextMenuRequested` → `onContextMenu`。节点类型用 `item->data(0, role)` 区分"三维体" vs "切片"(建树时按 ddCode:`dd_voxel/dd_Structual3D/dd_Property3D/dd_section` 为三维体;`dd_slice` 为切片)。右键分派(核心): ```cpp void Column3DAnalysis::onContextMenu(const QPoint& pos) { QTreeWidgetItem* it = tree_->itemAt(pos); if (!it) return; const QString dsId = it->data(0, kDsIdRole).toString(); const QString ddCode = it->data(0, kDsDdCodeRole).toString(); const QString name = it->data(0, kDsNameRole).toString(); const bool isSlice = (ddCode == QStringLiteral("dd_slice")); QMenu menu(this); if (!isSlice) { // 三维体数据集:切片▸(上下/前后/左右/任意) / 色阶 / 显示·隐藏 / 数据详情 QMenu* sub = menu.addMenu(QStringLiteral("切片")); using SA = geopro::render::interact::SliceAxis; sub->addAction(QStringLiteral("上下"), this, [this]{ emit sliceRequested(SA::UpDown); }); sub->addAction(QStringLiteral("前后"), this, [this]{ emit sliceRequested(SA::FrontBack); }); sub->addAction(QStringLiteral("左右"), this, [this]{ emit sliceRequested(SA::LeftRight); }); sub->addAction(QStringLiteral("任意"), this, [this]{ emit sliceRequested(SA::Oblique); }); menu.addAction(QStringLiteral("色阶"), this, [this, dsId]{ emit colorScaleRequested(dsId); }); menu.addAction(QStringLiteral("显示 / 隐藏"), this, [this, dsId]{ emit visibilityToggled(dsId); }); menu.addAction(QStringLiteral("数据详情"), this, [this, dsId, ddCode, name]{ emit detailRequested(dsId, ddCode, name); }); } else { // 切片数据集:保存/保存为/导出/删除 — 色阶/显示·隐藏/数据详情 menu.addAction(QStringLiteral("保存"), this, [this, dsId]{ emit sliceSaveRequested(dsId); }); menu.addAction(QStringLiteral("保存为"), this, [this, dsId]{ emit sliceSaveAsRequested(dsId); }); menu.addAction(QStringLiteral("导出"), this, [this, dsId]{ emit sliceExportRequested(dsId); }); menu.addAction(QStringLiteral("删除"), this, [this, dsId]{ emit sliceDeleteRequested(dsId); }); menu.addSeparator(); menu.addAction(QStringLiteral("色阶"), this, [this, dsId]{ emit colorScaleRequested(dsId); }); menu.addAction(QStringLiteral("显示 / 隐藏"), this, [this, dsId]{ emit visibilityToggled(dsId); }); menu.addAction(QStringLiteral("数据详情"), this, [this, dsId, ddCode, name]{ emit detailRequested(dsId, ddCode, name); }); } menu.exec(tree_->viewport()->mapToGlobal(pos)); } ``` > `kDsIdRole/kDsDdCodeRole/kDsNameRole`:用 DatasetListPanel 的公开 role 常量(读 DatasetListPanel.hpp)。树构建:用 `populateDatasetList(tree_, analysisRows, false)` 起步(它已按 parentId 建树:切片挂三维体下),再补可勾选标志(同 Task3 注 2)。 - [ ] **Step 3: 注册 CMake + 编译** `src/app/CMakeLists.txt` 加 `panels/columns/Column3DAnalysis.cpp`。 Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat app"` Expected: 编译通过。 - [ ] **Step 4: 提交** ```bash git add src/app/panels/columns/Column3DAnalysis.hpp src/app/panels/columns/Column3DAnalysis.cpp src/app/CMakeLists.txt git commit -m "feat(vtk): 三维分析栏 widget(对象→三维体→切片树+两套右键菜单)" ``` --- ## Task 6: 抽屉容器 ColumnDrawer [UI] **Files:** - Create: `src/app/panels/columns/ColumnDrawer.hpp/.cpp` - Modify: `src/app/CMakeLists.txt` - [ ] **Step 1: 写头文件** `ColumnDrawer.hpp`: ```cpp #pragma once #include namespace geopro::app { class Column3DDataset; class Column2DDataset; class Column3DAnalysis; // VTK视图左侧内嵌抽屉:三 tab(三维数据集/二维数据集/三维分析) + 折叠开关。 class ColumnDrawer : public QWidget { Q_OBJECT public: explicit ColumnDrawer(QWidget* parent = nullptr); Column3DDataset* col3D() const { return col3D_; } Column2DDataset* col2D() const { return col2D_; } Column3DAnalysis* colAnalysis() const { return colAnalysis_; } public slots: void toggleCollapsed(); // 折叠/展开(宽度切换) private: Column3DDataset* col3D_ = nullptr; Column2DDataset* col2D_ = nullptr; Column3DAnalysis* colAnalysis_ = nullptr; QWidget* body_ = nullptr; // QTabWidget 容器,折叠时隐藏 bool collapsed_ = false; }; } ``` - [ ] **Step 2: 写实现** `QTabWidget` 三页(三维数据集/二维数据集/三维分析)放入 `body_`;旁边一个细长折叠按钮(◀/▶,调 `toggleCollapsed`)。`toggleCollapsed`:`collapsed_ = !collapsed_; body_->setVisible(!collapsed_);` 并切按钮箭头。固定展开宽度约 300(`setFixedWidth` 或 `setMaximumWidth`,折叠时设 0/隐藏 body)。 - [ ] **Step 3: 注册 CMake + 编译** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat app"` Expected: 编译通过。 - [ ] **Step 4: 提交** ```bash git add src/app/panels/columns/ColumnDrawer.hpp src/app/panels/columns/ColumnDrawer.cpp src/app/CMakeLists.txt git commit -m "feat(vtk): ColumnDrawer 抽屉容器(三tab+折叠)" ``` --- ## Task 7: main.cpp 装配——删三浮层/切换,挂抽屉,接信号,改名 [UI] 这是核心整合。**一次性**替换,因 axisBar/sliceBar/layerPanel 的 connect 互相牵连,无法逐控件保持编译。改完一次编绿。 **Files:** - Modify: `src/app/main.cpp`(多处,见下) - [ ] **Step 1: 删旧 UI 构造** 删除: - `layerPanel` 块(393-429)、`axisBar` 块(433-516)、`RightTopAnchor`(520)、`sliceBar` 块(525-556)、`BottomLeftAnchor`(556)。 - 分段切换:`buildSegmentedHeader`/`viewHeader`/`act2D`/`act3D`(380-389)改为**简单标题头**(见 Step 3)。 - `showLayerPanel` lambda(804-827)及其所有调用。 - `updateSliceButtons`(559-569)、`addSlice`(572-575)、sliceBar 按钮 connect(576-590)。 - 旧 connect:layer checkboxes(846-851)、axisBar 控件(857-889)、act2D/act3D(832-843)。 - 保留 `interactionMgr` 创建(309-321)、`emptyState`、`sceneCtrl`、`vtkWidget`。 - [ ] **Step 2: 建 ColumnDrawer + 改 centerWidget 布局为 [抽屉 | GL]** 在 `centerWidget` 构造处(374 一带)改为:顶部一个标题头(Step 3),下面一个 `QHBoxLayout` 装 `drawer` + `vtkWidget`: ```cpp #include "app/panels/columns/ColumnDrawer.hpp" #include "app/panels/columns/Column3DDataset.hpp" #include "app/panels/columns/Column2DDataset.hpp" #include "app/panels/columns/Column3DAnalysis.hpp" // ... auto* drawer = new geopro::app::ColumnDrawer(centerWidget); auto* viewRow = new QHBoxLayout(); viewRow->setContentsMargins(0,0,0,0); viewRow->setSpacing(0); viewRow->addWidget(drawer); // 左侧抽屉 viewRow->addWidget(vtkWidget, 1); // 右侧 GL 画布 // centerLayout: [标题头] + [viewRow] centerLayout->addWidget(viewHeader); // Step 3 的新标题头 centerLayout->addLayout(viewRow, 1); ``` > 设计:抽屉是 vtkWidget 的**布局兄弟**(非 GL 子浮层),规避 `main.cpp:397-399` 注释提到的原生 GL 浮层圆角/底色伪影。视觉等同原型(栏在左、画布在右)。 - [ ] **Step 3: 新标题头(含全屏按钮,Task 8 接线)** 替换分段头为 `buildPanelHeader`: ```cpp auto* viewHeader = geopro::app::buildPanelHeader( geopro::app::Glyph::Map, QStringLiteral("VTK视图"), {{geopro::app::Glyph::Fullscreen, QStringLiteral("全屏")}}); ``` (全屏按钮的 connect 在 Task 8。) - [ ] **Step 4: 接三维数据集栏信号 → VtkSceneController** ```cpp auto* c3 = drawer->col3D(); QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl, &VtkSceneController::setAxesMode); QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl, &VtkSceneController::setAxesUnit); QObject::connect(c3, &geopro::app::Column3DDataset::verticalExaggerationChanged, sceneCtrl, &VtkSceneController::setVerticalExaggeration); QObject::connect(c3, &geopro::app::Column3DDataset::viewRequested, sceneCtrl, &VtkSceneController::applyView); QObject::connect(c3, &geopro::app::Column3DDataset::zoomInRequested, sceneCtrl, &VtkSceneController::zoomIn); QObject::connect(c3, &geopro::app::Column3DDataset::zoomOutRequested, sceneCtrl, &VtkSceneController::zoomOut); QObject::connect(c3, &geopro::app::Column3DDataset::fitRequested, sceneCtrl, &VtkSceneController::fit); // O点位置/字体本期 stub:connect 到一个提示(可空 lambda)。 ``` > 类型匹配:`Column3DDataset` 的枚举即 `geopro::controller::AxesMode/AxesUnit/ViewDir`(同 I3dSceneView.hpp),与 `setAxesMode/setAxesUnit/applyView` 形参一致,可直接连。 - [ ] **Step 5: 接三维分析栏「切片」→ InteractionManager** ```cpp auto* ca = drawer->colAnalysis(); QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceRequested, vtkWidget, [interactionMgr](geopro::render::interact::SliceAxis axis){ interactionMgr->addSlice(axis); }); QObject::connect(ca, &geopro::app::Column3DAnalysis::detailRequested, &detailCtrl, [&detailCtrl](const QString& dsId, const QString& ddCode, const QString& name){ detailCtrl.openDataset(dsId, ddCode, name); }); // colorScale/visibility/slice CRUD 本期 stub:connect 到提示 lambda(如 statusBar 显"待实现")。 ``` - [ ] **Step 6: 编译(维度过滤接线在 Task 9,此处先空列表)** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat rebuild"` Expected: 全量编译通过,exe 刷新。 - [ ] **Step 7: 用户实测清单**(用户在其终端跑) - [ ] app 启动,中央改名「VTK视图」,左侧出现三 tab 抽屉。 - [ ] 旧「二维地图/三维视图」分段按钮已消失;左上/右上/左下三浮层消失。 - [ ] 抽屉折叠开关:点 ◀ 收起、画布变宽;点 ▶ 展开。 - [ ] 三维数据集栏工具条:坐标轴下拉/比例滑块/快捷视图 6 钮/缩放 3 钮可点(功能接通后续 Task 9 验,但点击不崩)。 - [ ] 三维分析栏右键三维体 → 出「切片▸(上下/前后/左右/任意)/色阶/显隐/详情」;右键切片 → 出「保存/保存为/导出/删除/色阶/显隐/详情」。 - [ ] **Step 8: 提交** ```bash git add src/app/main.cpp git commit -m "refactor(vtk): 删三浮层+分段切换,改挂三栏抽屉,接信号,中央改名VTK视图" ``` --- ## Task 8: dockState 版本 bump + 全屏按钮 [UI] **Files:** - Modify: `src/app/main.cpp`(dockState 键 1428-1442;全屏 connect) - [ ] **Step 1: bump dock 布局版本** `main.cpp:1430` 与 1440:把 `ui/dockState_v2` 两处改为 `ui/dockState_v3`(dock 名/结构已变,旧布局须丢弃回落默认排布;遵循 1428-1430 注释)。 - [ ] **Step 2: 全屏切换实现** 全屏 = 隐藏其余 dock,仅留目标 dock 充满 dock 区;再点还原。用 ADS `CDockWidget::toggleView(bool)`。加一个 lambda + 状态: ```cpp // 全屏:隐藏其余 dock,仅留 target;再点还原。docks 列表见 hideDockTitleBars(733-740)。 bool* vtkFs = new bool(false); // 或用 QObject property,避免裸 new:可挂到 window auto makeFullscreen = [dockManager](ads::CDockWidget* target, const QList& others, bool on){ for (ads::CDockWidget* d : others) d->toggleView(!on); // on→隐藏其余 Q_UNUSED(target); }; ``` > 落地建议:用 `QToolButton::setCheckable(true)` 的全屏按钮 + `toggled(bool)` 切换;状态存按钮 checked,免裸指针。`others` = 除目标外的全部 dock(vtkDock 全屏时 others={leftDock,datasetDock,detailDock,rightDock,propDock};detailDock 全屏时 others={其余})。 - [ ] **Step 3: 接全屏按钮** 用 `findHeaderAction(box, Glyph::Fullscreen)`(main.cpp:1016-1020 的 helper)取到 VTK视图标题头与 数据详情头里的全屏按钮,connect 到 `makeFullscreen`。VTK视图头是 Step3(Task7) 的 `viewHeader`;数据详情头是 `detailDock` 的 `wrapWithHeader`(654-663)——给它加 `{{Glyph::Fullscreen,"全屏"}}` action。 ```cpp // 数据详情头加全屏 action(修改 654-663 的 wrapWithHeader 调用,加 actions 参数)。 // 然后: auto* vtkFsBtn = findHeaderAction(viewHeader, geopro::app::Glyph::Fullscreen); auto* detFsBtn = findHeaderAction(detailHeaderBox, geopro::app::Glyph::Fullscreen); // 各自 setCheckable(true) + connect(&QToolButton::toggled, ... makeFullscreen ...) ``` - [ ] **Step 4: 编译** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat rebuild"` Expected: 编译通过。 - [ ] **Step 5: 用户实测清单** - [ ] 点 VTK视图标题栏右侧全屏按钮 → VTK视图充满工作区(其余 dock 隐藏);再点 → 还原。 - [ ] 点 数据详情标题栏全屏按钮 → 同理。 - [ ] 首次启动(旧布局丢弃)dock 排布为默认,无错位。 - [ ] **Step 6: 提交** ```bash git add src/app/main.cpp git commit -m "feat(vtk): dockState bump v3 + VTK视图/数据详情 全屏按钮(隐藏其余dock)" ``` --- ## Task 9: 维度过滤接线——三栏数据集列表数据驱动 [UI+逻辑] 把"勾选对象 → 取 ds → 按维度分三栏"接通,替换 `main.cpp:891-899` 的 "grid1" 假实现。 **Files:** - Modify: `src/app/main.cpp` - [ ] **Step 1: 取勾选对象的 ds 行** 现状:`checkedTmsChanged(QStringList tmIds)` → 假 "grid1"。改为:用 `WorkbenchNavController`/`repo_.loadRowsAsync` 对每个勾选 TM 取 `DsRow`,汇总。`nav` 已有 `datasetsLoaded(tmObjectId, rows, total, append)` 信号(WorkbenchNavController.hpp:52)。**最简路径**:复用 nav 的取数,但 nav 现按"单击对象"取数(selectObject),勾选多 TM 需逐个取并合并。 实现:在 `checkedTmsChanged` 槽里,对每个 tmId 调 `nav.selectObject(tmId, 2)` 不合适(会刷左下列表)。改为直接调 repo: ```cpp // 汇总所有勾选 TM 的 ds,按维度分三栏。projectRepo 是 IAsyncProjectRepository。 QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &window, [&projectRepo, drawer, sceneCtrl, emptyState, &window](const QStringList& tmIds){ emptyState->setVisible(tmIds.isEmpty()); auto acc = std::make_shared>(); auto remaining = std::make_shared(tmIds.size()); if (tmIds.isEmpty()) { drawer->col3D()->setDatasets({}); drawer->col2D()->setDatasets({}); drawer->colAnalysis()->setDatasets({}); sceneCtrl->setCheckedDatasets({}); return; } for (const QString& tm : tmIds) { // classifyType=3, pageNo=1, 大 pageSize 取整树(同 WorkbenchNavController kFetchAllPageSize) geopro::data::NavRequest* req = projectRepo.loadRowsAsync( currentProjectIdStdString, tm.toStdString(), /*parentConfType*/2, /*classifyType*/3, 1, 100000); req->onDone = [acc, remaining, drawer](const geopro::data::DsPage& page){ acc->insert(acc->end(), page.rows.begin(), page.rows.end()); if (--(*remaining) == 0) { geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc); drawer->col3D()->setDatasets(b.dim3D); drawer->col2D()->setDatasets(b.dim2D); drawer->colAnalysis()->setDatasets(b.analysis); } }; // req->onFail 同样 --remaining 并在归零时刷新(避免一个失败卡死)。 } }); #include "app/DatasetDimension.hpp" ``` > 注:`NavRequest` 的回调字段真名见 `src/data/repo/IAsyncProjectRepository.hpp` / NavRequest 定义(onDone/onFail 或 done/failed)——落地按真实字段。`currentProjectIdStdString` 取当前项目 id(main.cpp 里已有项目 id 来源,搜 `currentProjectId`/`projectId`)。 - [ ] **Step 2: 勾选数据集 → 渲染** 三栏列表勾选 → `setCheckedDatasets`。汇总三栏勾选的 dsId: ```cpp auto pushChecked = [drawer, sceneCtrl]{ QStringList ids; // 收集三栏当前勾选(各栏暴露 checkedDatasetsChanged;此处也可各自直接连) // 简化:各栏 checkedDatasetsChanged 直接 setCheckedDatasets(合并)。 }; QObject::connect(drawer->col3D(), &geopro::app::Column3DDataset::checkedDatasetsChanged, sceneCtrl, &VtkSceneController::setCheckedDatasets); // 若需三栏合并,改为聚合后再 setCheckedDatasets;本期可先只接 col3D(3D 渲染主路径)。 ``` > 本期渲染主路径是 3D 数据集(帘面/体素/地形),故先接 `col3D` 的勾选 → `setCheckedDatasets`。2D/分析渲染随各自维度后续完善。 - [ ] **Step 3: 编译** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat rebuild"` Expected: 编译通过。 - [ ] **Step 4: 用户实测清单** - [ ] 勾选含 ds 的对象 → 三维数据集栏列表出现 3D 维度 ds(样本 "剖面网格数据1" dd_section)。 - [ ] 勾选三维数据集栏里的 ds → 中央渲染帘面(原 grid1 路径效果)。 - [ ] 取消全部勾选 → 三栏列表清空、中央清场、引导层 emptyState 显示。 - [ ] (样本数据若无 2D/切片 ds,2D/分析栏为空属正常;可后续在 LocalSample 加样本演示。) - [ ] **Step 5: 提交** ```bash git add src/app/main.cpp git commit -m "feat(vtk): 三栏数据集列表按维度过滤数据驱动(替换grid1假实现)" ``` --- ## Task 10: 全量回归 + 收尾 [逻辑] - [ ] **Step 1: 全量 build + ctest** Run: `cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat rebuild && .\build.bat test"` Expected: 编译通过;ctest 全绿(≥ 223/223,含新 DatasetDimension.* 2 项)。 - [ ] **Step 2: 派 cpp-reviewer 审查本分支改动** 对 `src/app/panels/columns/*`、`DatasetDimension.*`、`main.cpp` diff 跑 cpp-reviewer,修 CRITICAL/HIGH(重点:裸 new/生命周期、信号槽断连、ADS toggleView 还原正确性、role 常量硬编码)。 - [ ] **Step 3: 对照 spec 验收** 逐条核对 `2026-06-16-vtk-3d-three-column-refactor-design.md` §0 IN 项全部落地、OUT 项为 stub/禁用。 - [ ] **Step 4: 用户最终实测**(完整走查实测清单 Task7/8/9) - [ ] **Step 5: 提交收尾(如有修改)** ```bash git add -A && git commit -m "chore(vtk): 三栏重构 review 修复 + 回归" ``` --- ## Self-Review(写计划后自查) **Spec 覆盖:** - A1 三栏 → Task 3-7 ✅;单一 VTK视图/删切换/改名 → Task 7 ✅;三浮层收编 → Task 7 ✅;维度过滤列表 → Task 2+9 ✅;三维数据集 4 栏位 → Task 3 ✅;二维 地图/2D视图/自定义Z → Task 4 ✅;三维分析树+两右键菜单+切片接 SliceTool → Task 5+7 ✅;全屏 → Task 1+8 ✅;自定义Z绝对高程 → Task 4(spec 已记) ✅;dock 版本 bump → Task 8 ✅。 - OUT 项(CRUD/色阶/底图/异常体/详情/任务)→ stub 信号(Task 5),不实现 ✅。 **占位符扫描:** 已用真实 API/行号;少数"读真实 role 常量/NavRequest 字段名"是**有意指向源文件**(避免硬编码错值),非 TODO。 **类型一致性:** `AxesMode/AxesUnit/ViewDir`(I3dSceneView.hpp) 跨 Task3/7 一致;`SliceAxis`(SlicePlaneMath.hpp) 跨 Task5/7 一致;`DsRow/DsPage/DimBuckets` 跨 Task2/3/9 一致;`splitByDimension` 签名 Task2 定义、Task9 使用一致。 **风险:** Task 9 的多 TM 异步汇总 + NavRequest 回调字段名是最大不确定点(落地前先读 IAsyncProjectRepository.hpp 确认);全屏 toggleView 还原需保证 dock 顺序/可见性正确(Task8 用户实测把关)。