refactor(app): 视图改为对象树勾选驱动+单一2D/3D相机, 体素作为dd_voxel数据集(去除三模式混乱)

This commit is contained in:
gaozheng 2026-06-07 22:29:21 +08:00
parent 39b97ffb70
commit 51f217e1dd
2 changed files with 155 additions and 122 deletions

View File

@ -4,9 +4,11 @@
// 数据docs/剖面网格数据的色阶数据2等文件/真实样本UTF-8 中文路径,经 QFile 读取)。 // 数据docs/剖面网格数据的色阶数据2等文件/真实样本UTF-8 中文路径,经 QFile 读取)。
#include <fstream> #include <fstream>
#include <map>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <vector>
#include <QActionGroup> #include <QActionGroup>
#include <QApplication> #include <QApplication>
@ -47,7 +49,7 @@
#include <QVTKOpenGLStereoWidget.h> #include <QVTKOpenGLStereoWidget.h>
#include <vtkGenericOpenGLRenderWindow.h> #include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImageData.h> #include <vtkImageData.h>
#include <vtkImagePlaneWidget.h> #include <vtkProp.h>
#include <vtkRenderWindowInteractor.h> #include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h> #include <vtkRenderer.h>
#include <vtkSmartPointer.h> #include <vtkSmartPointer.h>
@ -55,7 +57,13 @@
namespace { namespace {
// 从对象结构树构建 QTreeWidgetGS → TM → DS 三层DS 项在 UserRole 存 dsId。 // 角色DS 项的 dd 类型存在 UserRole+1dsId 存在 UserRole
constexpr int kRoleDsId = Qt::UserRole;
constexpr int kRoleDdType = Qt::UserRole + 1;
// 从对象结构树构建 QTreeWidgetGS → TM → DS 三层。
// DS 项可勾选复选框勾选驱动该数据集在场景中的显示UserRole 存 dsId、UserRole+1 存 ddType。
// 网格剖面dd_section默认勾选启动即显示。
void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gss) void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gss)
{ {
for (const auto& gs : gss) { for (const auto& gs : gss) {
@ -67,7 +75,13 @@ void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gs
for (const auto& ds : tm.dss) { for (const auto& ds : tm.dss) {
auto* dsItem = new QTreeWidgetItem(tmItem); auto* dsItem = new QTreeWidgetItem(tmItem);
dsItem->setText(0, QString::fromStdString(ds.name)); dsItem->setText(0, QString::fromStdString(ds.name));
dsItem->setData(0, Qt::UserRole, QString::fromStdString(ds.id)); dsItem->setData(0, kRoleDsId, QString::fromStdString(ds.id));
dsItem->setData(0, kRoleDdType, QString::fromStdString(ds.ddType));
// DS 项可勾选:勾选 = 在场景中显示该数据集。
dsItem->setFlags(dsItem->flags() | Qt::ItemIsUserCheckable);
// 网格剖面默认显示;其余默认不勾。
dsItem->setCheckState(
0, ds.ddType == "dd_section" ? Qt::Checked : Qt::Unchecked);
} }
} }
} }
@ -208,6 +222,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
centerLayout->setContentsMargins(0, 0, 0, 0); centerLayout->setContentsMargins(0, 0, 0, 0);
centerLayout->setSpacing(0); centerLayout->setSpacing(0);
// 工具条:仅「二维/三维」相机开关,作用于整个场景(不绑定内容)。互斥二选一,默认二维。
auto* viewToolBar = new QToolBar(); auto* viewToolBar = new QToolBar();
auto* cameraGroup = new QActionGroup(viewToolBar); auto* cameraGroup = new QActionGroup(viewToolBar);
cameraGroup->setExclusive(true); cameraGroup->setExclusive(true);
@ -218,23 +233,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
cameraGroup->addAction(act2D); cameraGroup->addAction(act2D);
cameraGroup->addAction(act3D); cameraGroup->addAction(act3D);
act2D->setChecked(true); // 默认二维 act2D->setChecked(true); // 默认二维
viewToolBar->addSeparator();
auto* actVoxel = viewToolBar->addAction(QStringLiteral("三维体素"));
// 三按钮做成统一互斥的"3 路视图模式":任意时刻恰一个勾选。
actVoxel->setCheckable(true);
cameraGroup->addAction(actVoxel);
centerLayout->addWidget(viewToolBar); centerLayout->addWidget(viewToolBar);
centerLayout->addWidget(vtkWidget, 1); centerLayout->addWidget(vtkWidget, 1);
// 交互切片 widgetdd_voxel 时对体素 vtkImageData 加可拖切面dd_slice auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维/三维视图"));
// 需 interactorrender window 已设),切回非体素视图时 Off。用 shared_ptr 跨 lambda 共享。
auto sliceWidget = std::make_shared<vtkSmartPointer<vtkImagePlaneWidget>>();
auto hideSlice = [sliceWidget]() {
if (sliceWidget->Get()) (*sliceWidget)->Off();
};
auto* vtkDock = new ads::CDockWidget(QStringLiteral("剖面视图"));
vtkDock->setWidget(centerWidget); vtkDock->setWidget(centerWidget);
dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock); dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
@ -255,124 +257,146 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
rightDock->setWidget(propLabel); rightDock->setWidget(propLabel);
dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock); dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
// 视图模式状态:当前数据集 id / 名称(供「二维/三维」按钮重渲染剖面用)。 // ── 勾选驱动显示(核心)──────────────────────────────────────────────
auto currentDsId = std::make_shared<QString>(); // 已显示数据集 → 其 VTK props 的映射。一个数据集可对应多个 prop如剖面 bands+edges
auto currentDsName = std::make_shared<QString>(); // 用 shared_ptr 让多个 lambda 共享同一映射props 由 renderer 引用计数保活。
using PropList = std::vector<vtkSmartPointer<vtkProp>>;
auto dsProps = std::make_shared<std::map<QString, PropList>>();
// 渲染当前数据集的【剖面】:按 *cameraMode 选相机;体素态切回剖面也走这里。 // 初始化守卫populateTree 会程序化 setCheckState触发 itemChanged
// Scene/renderWindow 按【裸指针值】捕获Scene 挂 window 父链、renderer 被 renderWindow // 也用于「点击 DS 自动勾选」等程序化改动期间,避免回调递归/重入。
// 引用计数持有生命周期覆盖事件循环。repo 由调用方保活。 auto building = std::make_shared<bool>(false);
auto renderSectionCurrent =
[&repo, scene, renderWindowPtr, applyCurrentCamera, hideSlice, propLabel, currentDsId, // 是否已有任意可见对象(用于决定首个对象时套用当前相机预设)。
currentDsName]() { auto hasVisible = [dsProps]() {
if (currentDsId->isEmpty()) return; // 还没有选中任何数据集 for (const auto& kv : *dsProps) {
const std::string dsId = currentDsId->toStdString(); if (!kv.second.empty()) return true;
const auto g = repo.loadGrid(dsId); }
const auto cs = repo.loadColorScale(dsId); return false;
hideSlice(); // 回到剖面视图:关闭可能存在的体素切片 };
scene->clear();
// 按 ddType 构建某数据集的 props 并加入 renderer。返回构建出的 props可能为空
// dd_sectionloadGrid + loadColorScale → banded contourbands + edges
// dd_voxel两条交叉剖面散点 → IDW 体素 → GPU 体绘制WaitCursor 包裹耗时构建)。
auto buildProps = [&repo, propLabel](const QString& dsId,
const QString& ddType) -> PropList {
PropList props;
if (ddType == "dd_section") {
const std::string id = dsId.toStdString();
const auto g = repo.loadGrid(id);
const auto cs = repo.loadColorScale(id);
const auto actors = geopro::render::buildGridContour(g, cs); const auto actors = geopro::render::buildGridContour(g, cs);
scene->addActor(actors.bands); if (actors.bands) props.push_back(actors.bands);
scene->addActor(actors.edges); if (actors.edges) props.push_back(actors.edges);
applyCurrentCamera(); // 按当前 2D/3D 模式重设相机 } else if (ddType == "dd_voxel") {
renderWindowPtr->Render();
propLabel->setText(QStringLiteral("数据集: %1\n网格: %2 x %3\nvmin / vmax: %4 / %5")
.arg(*currentDsName)
.arg(g.nx())
.arg(g.ny())
.arg(g.vmin)
.arg(g.vmax));
};
// 联动:点击 DS 项 → 记为当前数据集 → 默认回到二维剖面(勾「二维」+ 俯视)。
auto renderDataset = [currentDsId, currentDsName, cameraMode, act2D, renderSectionCurrent](
QTreeWidgetItem* item) {
const QString id = item->data(0, Qt::UserRole).toString();
if (id.isEmpty()) return; // GS/TM 节点无 dsId忽略
*currentDsId = id;
*currentDsName = item->text(0);
*cameraMode = CameraMode::Top2D;
act2D->setChecked(true); // 点数据集默认回到二维剖面
renderSectionCurrent();
};
QObject::connect(tree, &QTreeWidget::itemClicked, tree,
[renderDataset](QTreeWidgetItem* it, int) { renderDataset(it); });
// 视图模式:二维 = 剖面 + 俯视;三维 = 剖面 + 斜视。两者都重渲染当前剖面。
QObject::connect(act2D, &QAction::triggered, vtkWidget,
[cameraMode, renderSectionCurrent]() {
*cameraMode = CameraMode::Top2D;
renderSectionCurrent();
});
QObject::connect(act3D, &QAction::triggered, vtkWidget,
[cameraMode, renderSectionCurrent]() {
*cameraMode = CameraMode::Free3D;
renderSectionCurrent();
});
// 当前体素 vtkImageDatadd_slice 切面 widget 需复用,保活到下次重建)。
auto voxelImage = std::make_shared<vtkSmartPointer<vtkImageData>>();
// dd_voxel读两条交叉剖面 → IDW 体素 → GPU 体绘制 + 默认 3D 视角 + 可拖切片。
QObject::connect(actVoxel, &QAction::triggered, vtkWidget,
[&repo, scene, rendererPtr, renderWindowPtr, cameraMode, actVoxel, propLabel,
sliceWidget, voxelImage]() {
QApplication::setOverrideCursor(Qt::WaitCursor); QApplication::setOverrideCursor(Qt::WaitCursor);
const auto scatters = repo.loadVoxelScatters(); const auto scatters = repo.loadVoxelScatters();
const auto cs = repo.loadColorScale("grid1"); const auto cs = repo.loadColorScale("grid1"); // 体素复用 grid1 色阶
auto result = buildVoxelFromScatters(scatters, cs); auto result = buildVoxelFromScatters(scatters, cs);
QApplication::restoreOverrideCursor(); QApplication::restoreOverrideCursor();
if (!result.volume) { if (result.volume) props.push_back(result.volume);
propLabel->setText(QStringLiteral("dd_voxel: 无可用散点数据")); else propLabel->setText(QStringLiteral("dd_voxel: 无可用散点数据"));
return;
} }
return props;
};
// 关闭旧切片(避免引用已释放的旧 image // 勾选态变化:勾选 → 构建/显示该数据集 props取消 → 移除其 props。
if (sliceWidget->Get()) (*sliceWidget)->Off(); // 多个数据集可同时勾选并叠加共存(不 clear 场景,按数据集增删 props
// 已知限制:剖面用「距离-深度」局部坐标,体素用 GIS 局部坐标,二者坐标基不同,
scene->clear(); // 叠加时不会真正空间对齐(后续 curtain/坐标统一工作,见 STATUS §5
rendererPtr->AddViewProp(result.volume); QObject::connect(
*voxelImage = result.image; tree, &QTreeWidget::itemChanged, tree,
[dsProps, building, buildProps, hasVisible, applyCurrentCamera, rendererPtr,
// 体素 = 体块 + 3D 视角;高亮「三维体素」自身(互斥组会清掉 2D/3D renderWindowPtr](QTreeWidgetItem* item, int) {
*cameraMode = CameraMode::Free3D; if (*building) return; // 程序化改动期间不处理,避免初始化递归
actVoxel->setChecked(true); const QString dsId = item->data(0, kRoleDsId).toString();
geopro::render::applyFree3D(rendererPtr); if (dsId.isEmpty()) return; // GS/TM 节点(无 dsId忽略
rendererPtr->ResetCamera(); const QString ddType = item->data(0, kRoleDdType).toString();
renderWindowPtr->Render(); // 先渲一帧确保 interactor 就绪 const bool checked = item->checkState(0) == Qt::Checked;
// 切片面在稀疏体素(两测线"十字"、其余空)上多为全黑空区、徒增困惑,
// M1 暂不默认显示;待有意义的切片交互(沿数据面)再加回。
if (checked) {
const bool wasEmpty = !hasVisible();
auto it = dsProps->find(dsId);
if (it == dsProps->end() || it->second.empty()) {
// 首次显示:构建并加入 renderer。
PropList props = buildProps(dsId, ddType);
for (const auto& p : props) rendererPtr->AddViewProp(p);
(*dsProps)[dsId] = std::move(props);
} else {
// 之前隐藏过(保留了 props仅恢复可见。
for (const auto& p : it->second) p->SetVisibility(1);
}
if (wasEmpty) applyCurrentCamera(); // 首个可见对象 → 套用当前相机预设
rendererPtr->ResetCamera(); // 框住所有可见对象
} else {
// 取消勾选:从场景移除该数据集 props。
auto it = dsProps->find(dsId);
if (it != dsProps->end()) {
for (const auto& p : it->second) rendererPtr->RemoveViewProp(p);
dsProps->erase(it);
}
}
renderWindowPtr->Render(); renderWindowPtr->Render();
propLabel->setText(
QStringLiteral("dd_voxel 体素\n网格: %1 x %2 x %3\nvmin / vmax: %4 / %5\n"
"(拖动切面查看 dd_slice)")
.arg(result.nx)
.arg(result.ny)
.arg(result.nz)
.arg(result.vmin)
.arg(result.vmax));
}); });
// 默认渲染第一个 DS让窗口一打开就有图。 // ── 单击 DS → 更新右侧属性(与勾选区分;不改变可见性)────────────────
if (auto* first = tree->topLevelItemCount() > 0 ? tree->topLevelItem(0) : nullptr) { QObject::connect(
// 递归找到首个带 dsId 的项。 tree, &QTreeWidget::itemClicked, tree,
QTreeWidgetItem* dsItem = nullptr; [&repo, propLabel](QTreeWidgetItem* item, int) {
QList<QTreeWidgetItem*> stack{first}; const QString dsId = item->data(0, kRoleDsId).toString();
while (!stack.isEmpty() && !dsItem) { if (dsId.isEmpty()) return; // GS/TM 节点无属性
const QString ddType = item->data(0, kRoleDdType).toString();
const QString name = item->text(0);
if (ddType == "dd_section") {
const auto g = repo.loadGrid(dsId.toStdString());
propLabel->setText(
QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n"
"vmin / vmax: %4 / %5")
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax));
} else if (ddType == "dd_voxel") {
propLabel->setText(
QStringLiteral("数据集: %1\n类型: 三维体素 (dd_voxel)\n"
"说明: 两交叉剖面 IDW 体素")
.arg(name));
}
});
// ── 工具条「二维/三维」:仅切换整场景相机预设,不改变内容可见性 ──────────
QObject::connect(act2D, &QAction::triggered, vtkWidget,
[cameraMode, applyCurrentCamera, rendererPtr, renderWindowPtr]() {
*cameraMode = CameraMode::Top2D;
applyCurrentCamera();
rendererPtr->ResetCamera();
renderWindowPtr->Render();
});
QObject::connect(act3D, &QAction::triggered, vtkWidget,
[cameraMode, applyCurrentCamera, rendererPtr, renderWindowPtr]() {
*cameraMode = CameraMode::Free3D;
applyCurrentCamera();
rendererPtr->ResetCamera();
renderWindowPtr->Render();
});
// ── 启动默认:网格剖面 DS 已在 populateTree 中设为 Checked但 itemChanged
// 在 connect 之前触发故未渲染。这里在 connect 之后,对已勾选的 DS 主动触发一次显示。
{
QList<QTreeWidgetItem*> stack;
for (int i = 0; i < tree->topLevelItemCount(); ++i) stack.append(tree->topLevelItem(i));
while (!stack.isEmpty()) {
QTreeWidgetItem* cur = stack.takeFirst(); QTreeWidgetItem* cur = stack.takeFirst();
if (!cur->data(0, Qt::UserRole).toString().isEmpty()) { const QString dsId = cur->data(0, kRoleDsId).toString();
dsItem = cur; if (!dsId.isEmpty() && cur->checkState(0) == Qt::Checked) {
break; const QString ddType = cur->data(0, kRoleDdType).toString();
PropList props = buildProps(dsId, ddType);
const bool wasEmpty = !hasVisible();
for (const auto& p : props) rendererPtr->AddViewProp(p);
(*dsProps)[dsId] = std::move(props);
if (wasEmpty) applyCurrentCamera();
} }
for (int i = 0; i < cur->childCount(); ++i) stack.append(cur->child(i)); for (int i = 0; i < cur->childCount(); ++i) stack.append(cur->child(i));
} }
if (dsItem) { rendererPtr->ResetCamera();
tree->setCurrentItem(dsItem); renderWindowPtr->Render();
renderDataset(dsItem);
}
} }
} }

View File

@ -51,16 +51,25 @@ std::string LocalSampleRepository::readFile(const std::string& fileNameUtf8) con
} }
std::vector<GsNode> LocalSampleRepository::loadStructure() { std::vector<GsNode> LocalSampleRepository::loadStructure() {
DsNode ds; // 剖面网格数据集dd_section在对象树勾选后渲染 banded contour 帘面。
ds.id = kDsId; DsNode dsSection;
ds.name = u8"剖面网格数据1"; dsSection.id = kDsId;
ds.ddType = "grid"; dsSection.name = u8"剖面网格数据1";
dsSection.ddType = "dd_section";
// 三维体素数据集dd_voxel勾选后由两条交叉剖面散点 IDW 出体素并叠加显示。
// 注意:体素与剖面坐标基不同(见 main.cpp 渲染处注释 + STATUS §5暂不空间配准。
DsNode dsVoxel;
dsVoxel.id = "voxel1";
dsVoxel.name = u8"三维体素";
dsVoxel.ddType = "dd_voxel";
TmNode tm; TmNode tm;
tm.id = "tm-ert1"; tm.id = "tm-ert1";
tm.name = "ERT1"; tm.name = "ERT1";
tm.confCode = "ERT"; tm.confCode = "ERT";
tm.dss.push_back(std::move(ds)); tm.dss.push_back(std::move(dsSection));
tm.dss.push_back(std::move(dsVoxel));
GsNode gs; GsNode gs;
gs.id = "gs-1"; gs.id = "gs-1";