feat(app): 对象树->选中数据集->渲染剖面+属性 联动(本地样本)

This commit is contained in:
gaozheng 2026-06-07 20:39:45 +08:00
parent f48b9ebb8f
commit 519d0ed1df
2 changed files with 120 additions and 43 deletions

View File

@ -18,6 +18,7 @@ target_link_libraries(geopro_desktop PRIVATE
ads::qt6advanceddocking
nlohmann_json::nlohmann_json
geopro_core # Phase 1ColorScale
geopro_data # Phase 2 / /
)
vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES})

View File

@ -1,23 +1,25 @@
// M1 demo 工作台ADS 停靠 + QVTK 视图渲染真实 ERT 网格剖面(设计图 #18
// 端到端打通core(ColorScale) + render(VTK banded contour) + view(Qt/ADS)。
// 数据D:/dev/spike_data/grid.json + colorbar.json样本拷贝ASCII 路径)
// 真实数据加载Repository + 中文路径 + API属 Phase 2本处为可视化里程碑 demo
// M1 工作台Phase 2 / Task 4对象树 → 选中数据集 → 中央 QVTK 渲染剖面 + 右侧属性
// 端到端data(LocalSampleRepository 读真实中文路径样本) + core(Grid/ColorScale) +
// render(VTK banded contour) + view(Qt/ADS 三栏停靠)
// 数据docs/剖面网格数据的色阶数据2等文件/真实样本UTF-8 中文路径,经 QFile 读取)
#include <fstream>
#include <string>
#include <vector>
#include <QApplication>
#include <QFormLayout>
#include <QLabel>
#include <QMainWindow>
#include <QSurfaceFormat>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QWidget>
#include <DockManager.h>
#include <DockWidget.h>
#include <nlohmann/json.hpp>
#include "model/ColorScale.hpp" // Phase 1 core
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/LocalSampleRepository.hpp"
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
@ -34,53 +36,55 @@
#include <vtkProperty.h>
#include <vtkRenderer.h>
using json = nlohmann::json;
using geopro::core::AlphaScale;
using geopro::core::ColorScale;
using geopro::core::parseColor;
namespace {
// 读真实网格样本,建 banded contour actors填充面 + 黑色等值线),返回到 renderer。
void buildGridSection(vtkRenderer* ren, const std::string& dataDir)
// 把 core 模型Grid + ColorScale渲染为 banded contour填充面 + 黑色等值线)。
// 入参是已加载的 core 模型,不读文件(数据加载由 Repository 负责)。
void renderGrid(vtkRenderer* ren, const geopro::core::Grid& g, const geopro::core::ColorScale& cs)
{
json g = json::parse(std::ifstream(dataDir + "grid.json"))["data"];
auto x = g["x"].get<std::vector<double>>();
auto y = g["y"].get<std::vector<double>>();
auto v = g["v"].get<std::vector<std::vector<double>>>(); // [j=y][i=x]
const int nx = static_cast<int>(x.size()), ny = static_cast<int>(y.size());
ren->RemoveAllViewProps(); // 清旧 actor支持重复切换数据集
const int nx = g.nx(), ny = g.ny();
if (nx < 2 || ny < 2 || g.x.size() < 2 || g.y.size() < 2) {
ren->SetBackground(1, 1, 1);
ren->ResetCamera();
return;
}
vtkNew<vtkImageData> img;
img->SetDimensions(nx, ny, 1);
img->SetOrigin(x[0], y[0], 0.0);
img->SetSpacing(x[1] - x[0], y[1] - y[0], 1.0);
img->SetOrigin(g.x[0], g.y[0], 0.0);
img->SetSpacing(g.x[1] - g.x[0], g.y[1] - g.y[0], 1.0);
vtkNew<vtkDoubleArray> sc;
sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny);
for (int j = 0; j < ny; ++j)
for (int i = 0; i < nx; ++i)
sc->SetValue(static_cast<vtkIdType>(j) * nx + i, v[j][i]); // i 最快
sc->SetValue(static_cast<vtkIdType>(j) * nx + i, g.valueAt(i, j)); // i 最快
img->GetPointData()->SetScalars(sc);
// 色阶:用 Phase 1 core 的 ColorScale 解析 colorBar网格色阶 alpha=0-255
json cb = json::parse(std::ifstream(dataDir + "colorbar.json"))["data"]["properties"]["colorBar"];
ColorScale cs;
std::vector<double> levels;
for (auto& pr : cb) {
double val = std::stod(pr[0].get<std::string>());
cs.addStop(val, parseColor(pr[1].get<std::string>(), AlphaScale::Bit255));
levels.push_back(val);
// vmin/vmax 来自 Grid若退化==)则用数据极值兜底,避免 LUT/contour 退化。
double vmin = g.vmin, vmax = g.vmax;
if (vmin >= vmax) {
const auto& vals = g.values();
vmin = vals.empty() ? 0.0 : vals.front();
vmax = vmin;
for (double v : vals) {
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
}
if (vmin >= vmax) vmax = vmin + 1.0;
}
const double vmin = levels.front(), vmax = levels.back();
// 用 ColorScale 填一张精细 LUT256 级,按真实 colorBar 阶梯取色)
// 256 级 LUT按 ColorScale 阶梯取色。
const int N = 256;
vtkNew<vtkLookupTable> lut;
lut->SetNumberOfTableValues(N);
lut->SetTableRange(vmin, vmax);
for (int t = 0; t < N; ++t) {
double val = vmin + (vmax - vmin) * t / (N - 1);
auto c = cs.colorAt(val);
const double val = vmin + (vmax - vmin) * t / (N - 1);
const auto c = cs.colorAt(val);
lut->SetTableValue(t, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0);
}
lut->Build();
@ -90,8 +94,7 @@ void buildGridSection(vtkRenderer* ren, const std::string& dataDir)
vtkNew<vtkBandedPolyDataContourFilter> banded;
banded->SetInputConnection(surf->GetOutputPort());
banded->SetNumberOfContours(static_cast<int>(levels.size()));
for (int k = 0; k < static_cast<int>(levels.size()); ++k) banded->SetValue(k, levels[k]);
banded->GenerateValues(20, vmin, vmax);
banded->GenerateContourEdgesOn();
banded->SetScalarModeToValue();
@ -118,6 +121,25 @@ void buildGridSection(vtkRenderer* ren, const std::string& dataDir)
ren->ResetCamera();
}
// 从对象结构树构建 QTreeWidgetGS → TM → DS 三层DS 项在 UserRole 存 dsId。
void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gss)
{
for (const auto& gs : gss) {
auto* gsItem = new QTreeWidgetItem(tree);
gsItem->setText(0, QString::fromStdString(gs.name));
for (const auto& tm : gs.tms) {
auto* tmItem = new QTreeWidgetItem(gsItem);
tmItem->setText(0, QString::fromStdString(tm.name));
for (const auto& ds : tm.dss) {
auto* dsItem = new QTreeWidgetItem(tmItem);
dsItem->setText(0, QString::fromStdString(ds.name));
dsItem->setData(0, Qt::UserRole, QString::fromStdString(ds.id));
}
}
}
tree->expandAll();
}
} // namespace
int main(int argc, char* argv[])
@ -125,18 +147,21 @@ int main(int argc, char* argv[])
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
// 本地样本仓储(中文路径,末尾带 '/'readFile 直接拼文件名)。生命周期覆盖事件循环。
geopro::data::LocalSampleRepository repo(
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
QMainWindow window;
window.setWindowTitle(QStringLiteral("Geopro 3.0 — ERT 网格剖面 (M1 demo)"));
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
window.resize(1280, 800);
// 中央 QVTK 视图(指针供联动回调使用)。
auto* vtkWidget = new QVTKOpenGLStereoWidget();
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
vtkWidget->setRenderWindow(renderWindow);
vtkNew<vtkRenderer> renderer;
renderWindow->AddRenderer(renderer);
buildGridSection(renderer, "D:/dev/spike_data/");
auto* dockManager = new ads::CDockManager(&window);
window.setCentralWidget(dockManager);
@ -144,14 +169,65 @@ int main(int argc, char* argv[])
vtkDock->setWidget(vtkWidget);
dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
// 左 dock对象树。
auto* tree = new QTreeWidget();
tree->setHeaderLabel(QStringLiteral("对象"));
populateTree(tree, repo.loadStructure());
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象列表"));
leftDock->setWidget(new QLabel(QStringLiteral("Phase 2 接入)")));
leftDock->setWidget(tree);
dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
// 右 dock属性。
auto* propLabel = new QLabel(QStringLiteral("(选择左侧数据集查看属性)"));
propLabel->setWordWrap(true);
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
propLabel->setMargin(8);
auto* rightDock = new ads::CDockWidget(QStringLiteral("数据集 / 属性"));
rightDock->setWidget(new QLabel(QStringLiteral("Phase 2 接入)")));
rightDock->setWidget(propLabel);
dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
// 联动:点击 DS 项 → 加载 grid/colorScale → 渲染 + 更新属性。
// [&] 捕获repo / renderer / renderWindow / propLabel 均在 main 作用域,
// 生命周期覆盖到 app.exec() 返回,事件回调期间安全。
auto renderDataset = [&](QTreeWidgetItem* item) {
const QString id = item->data(0, Qt::UserRole).toString();
if (id.isEmpty()) return; // GS/TM 节点无 dsId忽略
const std::string dsId = id.toStdString();
const auto g = repo.loadGrid(dsId);
const auto cs = repo.loadColorScale(dsId);
renderGrid(renderer, g, cs);
renderWindow->Render();
propLabel->setText(QStringLiteral("数据集: %1\n网格: %2 x %3\nvmin / vmax: %4 / %5")
.arg(item->text(0))
.arg(g.nx())
.arg(g.ny())
.arg(g.vmin)
.arg(g.vmax));
};
QObject::connect(tree, &QTreeWidget::itemClicked, tree,
[&](QTreeWidgetItem* it, int) { renderDataset(it); });
window.show();
// 默认渲染第一个 DS让窗口一打开就有图。
if (auto* first = tree->topLevelItemCount() > 0 ? tree->topLevelItem(0) : nullptr) {
// 递归找到首个带 dsId 的项。
QTreeWidgetItem* dsItem = nullptr;
QList<QTreeWidgetItem*> stack{first};
while (!stack.isEmpty() && !dsItem) {
QTreeWidgetItem* cur = stack.takeFirst();
if (!cur->data(0, Qt::UserRole).toString().isEmpty()) {
dsItem = cur;
break;
}
for (int i = 0; i < cur->childCount(); ++i) stack.append(cur->child(i));
}
if (dsItem) {
tree->setCurrentItem(dsItem);
renderDataset(dsItem);
}
}
return app.exec();
}