41 KiB
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, 之后加:
Collapse, // 折叠(双箭头)
Fullscreen, // 全屏 / 最大化
- Step 2: 在 Glyphs.cpp 的 svg 映射加 case
找到 makeGlyph/svg path 的 switch(参照 case Glyph::Collapse:),加:
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: 提交
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:
#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:
#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:
#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: 提交
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:
#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 ...。要点(完整骨架):
#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: 提交
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:
#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: 提交
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:
#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 为切片)。右键分派(核心):
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: 提交
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:
#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: 提交
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)。 -
showLayerPanellambda(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:
#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:
auto* viewHeader = geopro::app::buildPanelHeader(
geopro::app::Glyph::Map, QStringLiteral("VTK视图"),
{{geopro::app::Glyph::Fullscreen, QStringLiteral("全屏")}});
(全屏按钮的 connect 在 Task 8。)
- Step 4: 接三维数据集栏信号 → VtkSceneController
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
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: 提交
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 + 状态:
// 全屏:隐藏其余 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= 除目标外的全部 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。
// 数据详情头加全屏 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: 提交
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:
// 汇总所有勾选 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取当前项目 id(main.cpp 里已有项目 id 来源,搜currentProjectId/projectId)。
- Step 2: 勾选数据集 → 渲染
三栏列表勾选 → setCheckedDatasets。汇总三栏勾选的 dsId:
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: 提交
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: 提交收尾(如有修改)
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 用户实测把关)。