geopro/docs/superpowers/plans/2026-06-16-vtk-3d-three-col...

870 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 vtkDockbump 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("<path d='M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3"
"M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3'/>");
```
(若已有 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 <gtest/gtest.h>
#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<DsRow> 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 <vector>
#include "data/repo/RepoTypes.hpp"
namespace geopro::app {
struct DimBuckets {
std::vector<geopro::data::DsRow> dim3D;
std::vector<geopro::data::DsRow> dim2D;
std::vector<geopro::data::DsRow> analysis;
};
// 按 ddCode 把 ds 分流到 三维数据集 / 二维数据集 / 三维分析 三栏。
// Other 维度不入任何栏(保留 parentId 顺序,调用方可直接喂 populateDatasetList
DimBuckets splitByDimension(const std::vector<geopro::data::DsRow>& 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<geopro::data::DsRow>& 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]
独立 widget4 工具条栏位(坐标轴设置 / 水平垂直比例 / 快捷视图 / 缩放)+ 数据集树。只发信号。控件创建可**搬运** `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 <QWidget>
#include <QStringList>
#include <vector>
#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<geopro::data::DsRow>& 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 <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QComboBox>
#include <QSlider>
#include <QPushButton>
#include <QLabel>
#include <QTreeWidget>
#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<int>(AxesMode::Standard));
mode->addItem(QStringLiteral("三维立体"), static_cast<int>(AxesMode::Stereo));
mode->addItem(QStringLiteral("不显示"), static_cast<int>(AxesMode::None));
connect(mode, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this, mode](int){ emit axesModeChanged(static_cast<AxesMode>(mode->currentData().toInt())); });
auto* oPoint = new QPushButton(QStringLiteral("设置…"));
connect(oPoint, &QPushButton::clicked, this, &Column3DDataset::oPointClicked);
auto* unit = new QComboBox();
unit->addItem(QStringLiteral("无刻度"), static_cast<int>(AxesUnit::None));
unit->addItem(QStringLiteral("米"), static_cast<int>(AxesUnit::Meter));
unit->addItem(QStringLiteral("英尺"), static_cast<int>(AxesUnit::Feet));
unit->addItem(QStringLiteral("经纬度"), static_cast<int>(AxesUnit::LatLon));
unit->setCurrentIndex(1);
connect(unit, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this, unit](int){ emit axesUnitChanged(static_cast<AxesUnit>(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<double>(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<geopro::data::DsRow>& 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 <QWidget>
#include <QStringList>
#include <vector>
#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<geopro::data::DsRow>& 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 <QWidget>
#include <QStringList>
#include <vector>
#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<geopro::data::DsRow>& 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 <QWidget>
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` lambda804-827及其所有调用。
- `updateSliceButtons`559-569、`addSlice`572-575、sliceBar 按钮 connect576-590
- 旧 connectlayer checkboxes846-851、axisBar 控件857-889、act2D/act3D832-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点位置/字体本期 stubconnect 到一个提示(可空 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 本期 stubconnect 到提示 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<ads::CDockWidget*>& others, bool on){
for (ads::CDockWidget* d : others) d->toggleView(!on); // on→隐藏其余
Q_UNUSED(target);
};
```
> 落地建议:用 `QToolButton::setCheckable(true)` 的全屏按钮 + `toggled(bool)` 切换;状态存按钮 checked免裸指针。`others` = 除目标外的全部 dockvtkDock 全屏时 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<std::vector<geopro::data::DsRow>>();
auto remaining = std::make_shared<int>(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` 取当前项目 idmain.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/切片 ds2D/分析栏为空属正常;可后续在 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 用户实测把关)。