feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
14 changed files with 61 additions and 31 deletions
Showing only changes of commit 8f167b62c9 - Show all commits

View File

@ -1061,7 +1061,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString();
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName);
// tmObjectId白化 structParentId从行读出透传使白化模板列表非空。
const QString tmObjectId =
item->data(0, geopro::app::kDsTmObjectIdRole).toString();
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId);
});
// ── 控制器信号 → 详情面板tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ──
@ -1468,10 +1471,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (dsId.isEmpty()) return;
const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString();
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
// tmObjectId白化 structParentId从行读出透传使白化模板列表非空。
const QString tmObjectId = item->data(0, geopro::app::kDsTmObjectIdRole).toString();
QMenu menu(datasetList);
menu.addAction(QStringLiteral("数据集详情"), datasetList,
[&detailCtrl, dsId, ddCode, dsName]() {
detailCtrl.openDataset(dsId, ddCode, dsName);
[&detailCtrl, dsId, ddCode, dsName, tmObjectId]() {
detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId);
});
menu.addAction(QStringLiteral("属性"), datasetList, [&nav, dsId]() {
nav.selectDataset(dsId); // 只读元字段
@ -1636,10 +1641,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) {
const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows,
int total, bool append) {
removeTreeLoadMore(datasetList);
geopro::app::populateDatasetList(datasetList, rows, append);
// tmObjectId本批所属 TM 对象 id存入每项 → 白化对话框透传用structParentId
geopro::app::populateDatasetList(datasetList, rows, append, tmObjectId);
const int loaded = addTreeLoadMore(datasetList, total);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
datasetTabs->setTabText(

View File

@ -50,7 +50,8 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const
// 仓储与 projectId 回调透传给工厂FilledContour 用色阶模板仓储Scatter 用反演命令仓储)。
// dsIdGetter 用本页 dsId_此处已赋值随项目/数据集稳定。
auto view = makeDetailView(spec.kind, this, colorTplRepo_, projectIdGetter_, cmdRepo_,
[this] { return dsId_; }); // 抛出由调用栈兜底GuardedApplication
[this] { return dsId_; },
[this] { return tmObjectId_; }); // 抛出由调用栈兜底GuardedApplication
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
views_[i] = raw;
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget随其尺寸覆盖图区

View File

@ -32,6 +32,9 @@ public:
// dsId 用本页 dsId_build 内构造 dsIdGetter此时 dsId_ 已赋值projectId 复用上面的 getter。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
// 所属 TM 对象 id=白化 structParentId注入须在 build 前设置 → tmObjectIdGetter 透传给视图)。
void setTmObjectId(const QString& tmObjectId) { tmObjectId_ = tmObjectId; }
// 按页签集构建页签首次打开调一次。dsId/ddCode/dsName 用于 tabNeeded。
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
const std::vector<geopro::controller::TabSpec>& tabs);
@ -57,6 +60,7 @@ private:
QString dsId_;
QString ddCode_;
QString dsName_;
QString tmObjectId_; // 所属 TM 对象 id白化 structParentId经 tmObjectIdGetter 透传给视图
std::vector<geopro::controller::TabSpec> tabs_;
// 与 tabs_ 同序。每个 IDetailView 持有的 QWidget 经 build() 以 this 为父接管,
// 生命周期由 Qt 父子树清理(不在此 deletebuild() 仅调用一次(见其断言)。

View File

@ -32,7 +32,7 @@ DatasetDetailPage* DatasetDetailPanel::pageFor(const QString& dsId) const {
}
void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddCode,
const QString& dsName,
const QString& dsName, const QString& tmObjectId,
const std::vector<geopro::controller::TabSpec>& tabs) {
auto* p = pageFor(dsId);
if (!p) {
@ -40,6 +40,7 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC
// 注入须在 build 前build 内造视图时即透传给工厂)。
p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_);
p->setCommandRepo(cmdRepo_);
p->setTmObjectId(tmObjectId); // 白化 structParentIdbuild 前设置 → 透传给视图)
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id
const int idx = addTab(p, title);

View File

@ -26,7 +26,9 @@ public:
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
// 数据集打开find-or-create 页 → build(tabs) → 加/抬该面板页签。
// tmObjectId所属 TM 对象 id白化 structParentIdbuild 前交给页 → 视图。
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
const QString& tmObjectId,
const std::vector<geopro::controller::TabSpec>& tabs);
void onTabReady(const QString& dsId, int tabIndex, const QVariant& payload);
void onTabLoadStarted(const QString& dsId, int tabIndex);

View File

@ -160,7 +160,7 @@ public:
namespace {
// 建一条数据集树项不挂载列0 文本 = dsName +「创建时间 · 类型名」data 存各角色。
QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) {
QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d, const QString& tmObjectId) {
QString text = QString::fromStdString(d.dsName);
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
if (!d.typeName.empty())
@ -174,6 +174,7 @@ QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) {
item->setData(0, kDsNameRole, QString::fromStdString(d.dsName));
item->setData(0, kDsTypeNameRole, QString::fromStdString(d.typeName));
item->setData(0, kDsCreateTimeRole, QString::fromStdString(d.createTime));
item->setData(0, kDsTmObjectIdRole, tmObjectId); // 所属 TM 对象 id白化 structParentId
// 单击 tip显示数据集主要属性名称 / 类型 / 创建时间对齐菜单文档「tip显示ds的主要属性」。
QString tip = QStringLiteral("名称:%1").arg(QString::fromStdString(d.dsName));
if (!d.typeName.empty()) tip += QStringLiteral("\n类型:%1").arg(QString::fromStdString(d.typeName));
@ -184,7 +185,8 @@ QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d) {
}
} // namespace
void populateDatasetList(QTreeWidget* tree, const std::vector<geopro::data::DsRow>& rows, bool append) {
void populateDatasetList(QTreeWidget* tree, const std::vector<geopro::data::DsRow>& rows, bool append,
const QString& tmObjectId) {
if (!tree) return;
if (!append) tree->clear();
@ -199,7 +201,7 @@ void populateDatasetList(QTreeWidget* tree, const std::vector<geopro::data::DsRo
std::vector<QTreeWidgetItem*> batch;
batch.reserve(rows.size());
for (const auto& d : rows) {
auto* item = makeDatasetItem(d);
auto* item = makeDatasetItem(d, tmObjectId);
byId.insert(QString::fromStdString(d.id), item);
batch.push_back(item);
}

View File

@ -22,11 +22,14 @@ constexpr int kDsDdCodeRole = 0x0104; // Qt::UserRole + 4ddCode双击详
constexpr int kDsNameRole = 0x0105; // Qt::UserRole + 5dsName详情页签标题用
constexpr int kDsTypeNameRole = 0x0106; // Qt::UserRole + 6类型名快速筛选用
constexpr int kDsCreateTimeRole = 0x0107; // Qt::UserRole + 7创建时间按日期筛选用
constexpr int kDsTmObjectIdRole = 0x0108; // Qt::UserRole + 8所属 TM 对象 id=白化 structParentId
// 数据页签:树形(按 DsRow.parentId 嵌套,源数据为根、派生数据挂其下,对齐原版 el-table 树)。
// 每项列0文本 = dsName +「创建时间 · 类型名」data(0,角色) 存 dsId/ddCode/dsName。
// append=true 时把新行挂到已加载的父节点下(分页)。
void populateDatasetList(QTreeWidget* tree, const std::vector<geopro::data::DsRow>& rows, bool append);
// tmObjectId本批数据所属 TM 对象 id=白化 structParentId存入每项 kDsTmObjectIdRole可空。
void populateDatasetList(QTreeWidget* tree, const std::vector<geopro::data::DsRow>& rows, bool append,
const QString& tmObjectId = QString());
// 文件页签:每条 = 文件名 +可读大小UserRole 存 dsId、+2 存文件 url。空时显示占位。
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);

View File

@ -16,7 +16,8 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
geopro::data::IColorTemplateRepository* colorTplRepo,
std::function<QString()> projectIdGetter,
geopro::data::IDatasetCommandRepository* cmdRepo,
std::function<QString()> dsIdGetter) {
std::function<QString()> dsIdGetter,
std::function<QString()> tmObjectIdGetter) {
switch (kind) {
case controller::ViewKind::Scatter: {
auto* raw = new RawDataChartView(parent);
@ -30,6 +31,8 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
grid->setColorTemplateRepo(colorTplRepo, projectIdGetter);
// 注入反演命令仓储 + dsId/projectId 取值回调(网格化/白化/滤波/另存为按钮)。
grid->setCommandRepo(cmdRepo, std::move(dsIdGetter), std::move(projectIdGetter));
// 注入 tmObjectId 取值回调(白化对话框模板列表用,= 数据集 structParentId
grid->setTmObjectIdGetter(std::move(tmObjectIdGetter));
return std::unique_ptr<IDetailView>(grid);
}
case controller::ViewKind::Table: {

View File

@ -22,11 +22,13 @@ class IDetailView;
// 现阶段命中会抛 std::runtime_error明确失败而非静默空指针
// colorTplRepo/projectIdGetterFilledContour 视图的色阶模板仓储注入(可空 → 编辑器后端按钮禁用)。
// cmdRepo/dsIdGetterScatter 视图measurement反演运算/生成视电阻率命令仓储注入(可空 → 按钮占位提示)。
// tmObjectIdGetterFilledContour 视图白化对话框所需 tmObjectId=数据集 structParentId取值回调可空 → 模板列表空)。
std::unique_ptr<IDetailView> makeDetailView(
controller::ViewKind kind, QWidget* parent,
geopro::data::IColorTemplateRepository* colorTplRepo = nullptr,
std::function<QString()> projectIdGetter = {},
geopro::data::IDatasetCommandRepository* cmdRepo = nullptr,
std::function<QString()> dsIdGetter = {});
std::function<QString()> dsIdGetter = {},
std::function<QString()> tmObjectIdGetter = {});
} // namespace geopro::app

View File

@ -381,16 +381,10 @@ void GridDataChartView::openWhitening() {
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
// tmObjectId白化模板列表用= 当前数据集的 structParentId对照原版 dsFileRow.structParentId
// 客户端 open 链路未透传该字段 → 方案 A懒拉 getDsObjectDetail(dsId) 取 structParentId
// 再打开白化对话框。即便取不到(空串)也照常打开(与原版兜底一致,仅模板列表为空)。
QPointer<GridDataChartView> self(this);
cmdRepo_->getDsObjectDetail(dsId, [self, dsId, projectId](bool ok, QJsonObject data, QString) {
if (!self) return;
const QString tmObjectId =
ok ? data.value(QStringLiteral("structParentId")).toString() : QString();
WhiteningDialog dlg(self->cmdRepo_, dsId, projectId, tmObjectId, self);
if (dlg.exec() == QDialog::Accepted) self->reloadGrid();
});
// 经 open 链路从数据集列表行透传至此(注入的 tmObjectIdGetter_。空串也照常打开仅模板列表为空
const QString tmObjectId = tmObjectIdGetter_ ? tmObjectIdGetter_() : QString();
WhiteningDialog dlg(cmdRepo_, dsId, projectId, tmObjectId, this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::openFilter() {

View File

@ -1,5 +1,6 @@
#pragma once
#include <functional>
#include <utility>
#include <vector>
#include <QString>
@ -64,6 +65,11 @@ public:
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter);
// 注入 tmObjectId 取值回调(= 数据集 structParentId。白化对话框模板列表用空 → 模板列表为空。
void setTmObjectIdGetter(std::function<QString()> tmObjectIdGetter) {
tmObjectIdGetter_ = std::move(tmObjectIdGetter);
}
private:
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
void openColorScaleEditor(); // 「色阶配置」→ 共享色阶编辑器(色阶 + 层级⚙ + 线形⚙)
@ -118,6 +124,9 @@ private:
// 反演命令仓储 + dsId 取值回调(注入;空则处理类按钮占位提示)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
std::function<QString()> dsIdGetter_;
// tmObjectId 取值回调(= 数据集 structParentId。白化对话框模板列表用空 → 模板列表为空。
std::function<QString()> tmObjectIdGetter_;
};
} // namespace geopro::app

View File

@ -19,9 +19,9 @@ DatasetDetailController::~DatasetDetailController() {
}
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode,
const QString& dsName) {
qInfo("[detail] openDataset id=%s ddCode=%s name=%s", qUtf8Printable(dsId),
qUtf8Printable(ddCode), qUtf8Printable(dsName));
const QString& dsName, const QString& tmObjectId) {
qInfo("[detail] openDataset id=%s ddCode=%s name=%s tm=%s", qUtf8Printable(dsId),
qUtf8Printable(ddCode), qUtf8Printable(dsName), qUtf8Printable(tmObjectId));
auto* s = registry_.find(ddCode.toStdString());
if (!s) { // 未注册策略 → 优雅降级
qWarning("[detail] 未注册策略 ddCode=%s → 降级提示", qUtf8Printable(ddCode));
@ -29,7 +29,7 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd
return;
}
const std::vector<controller::TabSpec> tabs = s->tabs();
emit datasetOpened(dsId, ddCode, dsName, tabs);
emit datasetOpened(dsId, ddCode, dsName, tmObjectId, tabs);
for (int i = 0; i < static_cast<int>(tabs.size()); ++i)
if (!tabs[static_cast<size_t>(i)].lazy) loadTab(dsId, ddCode, i);
}

View File

@ -21,7 +21,9 @@ public:
~DatasetDetailController() override; // 退出契约(spec §7)abort 全部在飞句柄,避免迟到信号打到已析构 this
public slots:
// 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。
void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString());
// tmObjectId数据集所属 TM 对象 id=白化 structParentId透传给详情页给白化对话框用可空。
void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString(),
const QString& tmObjectId = QString());
// 加载某页签lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。
// 分页型页签(如 dd_grid 列表首载用默认页pageNo=1/pageSize=0 → 仓储解析默认每页条数)。
void loadTab(const QString& dsId, const QString& ddCode, int tabIndex);
@ -31,7 +33,7 @@ public slots:
void focusDataset(const QString& dsId);
signals:
void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
const std::vector<controller::TabSpec>& tabs);
const QString& tmObjectId, const std::vector<controller::TabSpec>& tabs);
void tabLoadStarted(const QString& dsId, int tabIndex);
void tabReady(const QString& dsId, int tabIndex, const QVariant& payload);
void loadFailed(const QString& dsId, const QString& message);

View File

@ -62,12 +62,13 @@ TEST(DatasetDetailController, OpenEmitsDatasetOpenedWithTabsAndDdCode) {
auto reg = makeInversionRegistry();
controller::DatasetDetailController c(repo, reg);
QSignalSpy spy(&c, &controller::DatasetDetailController::datasetOpened);
c.openDataset("ds1", "dd_inversion_data", "名称");
c.openDataset("ds1", "dd_inversion_data", "名称", "tm1");
ASSERT_EQ(spy.count(), 1);
const auto args = spy.takeFirst();
EXPECT_EQ(args.at(0).toString(), QStringLiteral("ds1"));
EXPECT_EQ(args.at(1).toString(), QStringLiteral("dd_inversion_data")); // ddCode 透传
EXPECT_EQ(args.at(2).toString(), QStringLiteral("名称"));
EXPECT_EQ(args.at(3).toString(), QStringLiteral("tm1")); // tmObjectId 透传(白化 structParentId
}
TEST(DatasetDetailController, OpenLoadsNonLazyTabsOnly) {