feat(app): 工作台接入真实导航(空间/项目/对象树/DS),中央渲染占位

This commit is contained in:
gaozheng 2026-06-09 12:01:30 +08:00
parent 6241eb3a7e
commit 405fb2ae4f
1 changed files with 88 additions and 212 deletions

View File

@ -60,6 +60,10 @@
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include "TopBar.hpp"
#include "CentralScene.hpp"
#include "WorkbenchNavController.hpp"
#include "api/ApiProjectRepository.hpp"
#include "panels/ObjectTreePanel.hpp"
#include "login/LoginWindow.hpp"
#include "panels/AnomalyListPanel.hpp"
#include "panels/DatasetListPanel.hpp"
@ -95,41 +99,6 @@
namespace {
// 角色:树 TM 项存 tmId(UserRole+2);数据列表 DS 项的 dsId/ddType 由 panels/DatasetListPanel 定义。
constexpr int kRoleTmId = Qt::UserRole + 2;
// 从对象结构树构建 QTreeWidgetGS → TM 两层对齐原型DS=采集批次在左下「数据列表」,不进树)。
// TM(测线) 项可勾选(复选框):勾选驱动该测线的 dd_section 在中央场景显示UserRole+2 存 tmId。
// 含 dd_section 的测线默认勾选,启动即显示。
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));
tmItem->setData(0, kRoleTmId, QString::fromStdString(tm.id));
tmItem->setFlags(tmItem->flags() | Qt::ItemIsUserCheckable);
const bool hasSection =
std::any_of(tm.dss.begin(), tm.dss.end(),
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
tmItem->setCheckState(0, hasSection ? Qt::Checked : Qt::Unchecked);
}
}
tree->expandAll();
}
// 在结构中按 tmId 查 TM找不到返回 nullptr。
const geopro::data::TmNode* findTm(const std::vector<geopro::data::GsNode>& gss,
const std::string& tmId)
{
for (const auto& gs : gss)
for (const auto& tm : gs.tms)
if (tm.id == tmId) return &tm;
return nullptr;
}
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
std::string readPem(const std::string& path)
{
@ -150,7 +119,7 @@ double median(std::vector<double> v)
}
// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。
enum class ViewMode { Map2D, View3D };
using geopro::app::ViewMode;
// 数据详情显示内容(默认网格数据)。网格数据=#18 banded原数据=#17 散点(对齐原型命名)。
enum class DetailMode { Section18, Scatter17 };
@ -168,7 +137,8 @@ constexpr const char* kWgs84 = "EPSG:4326";
// 在给定 QMainWindow 上构建 M1 工作台。
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo)
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo,
geopro::controller::WorkbenchNavController& nav)
{
// ── 世界系:启动取一次 grid1 的 lat/lon用中位数作 GeoLocalFrame 原点 ──
// 全项目共享shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
@ -291,6 +261,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
chkTerrain->setEnabled(false); chkTerrain->setToolTip(tip);
chkSlice->setEnabled(false); chkSlice->setToolTip(tip);
}
// 本轮中央不接真实派生层:体素/切片/地形勾选置灰,待下一轮接入对应数据源。
for (QCheckBox* c : {chkVoxel, chkSlice, chkTerrain}) {
c->setEnabled(false);
c->setToolTip(QStringLiteral("(下一轮接入真实数据源)"));
}
layerLayout->addWidget(layerTitle);
layerLayout->addWidget(chkCurtain);
layerLayout->addWidget(chkVoxel);
@ -351,29 +326,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 放在中央视图下方。
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
// 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。
auto structure = std::make_shared<std::vector<geopro::data::GsNode>>(repo.loadStructure());
// 左上 dock对象树(GS→TM,测线复选)。表头交给自绘 PanelHeader隐藏树自带列头(避免双标题)。
auto* tree = new QTreeWidget();
tree->setHeaderHidden(true);
populateTree(tree, *structure);
// 选中行高亮不覆盖左侧缩进/折叠箭头列:给 branch 设白底(与树底一致),并用生成的箭头图片
// 保留展开/折叠图标(直接给 branch 设背景会触发 Qt 不再画默认箭头的陷阱)。
{
const QString openArrow = geopro::app::writeChevronIcon(true, QColor("#8A93A3"));
const QString closedArrow = geopro::app::writeChevronIcon(false, QColor("#8A93A3"));
tree->setStyleSheet(
QStringLiteral(
"QTreeView::branch { background: #FFFFFF; }"
"QTreeView::branch:has-children:!has-siblings:closed,"
"QTreeView::branch:closed:has-children:has-siblings { image: url(%1); }"
"QTreeView::branch:open:has-children:!has-siblings,"
"QTreeView::branch:open:has-children:has-siblings { image: url(%2); }")
.arg(closedArrow, openArrow));
}
// 左上 dock对象树真实结构项目根 → GS → TM。被动视图数据由控制器推送。
auto* objectTree = new geopro::app::ObjectTreePanel();
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"), tree,
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"),
objectTree,
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
@ -438,128 +395,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (auto* bar = area->titleBar()) bar->setVisible(false);
}
// ── 中央视图重建(核心)─────────────────────────────────────────────
// 按勾选的测线(TM)整体重建scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。
// 二维地图 = buildSurveyLine红线俯视浅底背景+ applyTop2D。
// 三维视图 = buildCurtain断面墙SetScale(1,1,kCurtainZScale) + applyFree3D白底
// frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree,
structure, showCurtain, showVoxel, showTerrain, showSlice, slicePlane,
crs, refElev]() {
// 先拆除上次的切片 widget(独立于 scene actor须显式关闭),再按条件重建。
if (*slicePlane) { (*slicePlane)->Off(); *slicePlane = nullptr; }
scene->clear();
const bool is2D = (*viewMode == ViewMode::Map2D);
rendererPtr->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0);
// 渲染单个 dd_section 数据集:二维=测线线;三维=帘面(受「帘面」图层开关控制)。
auto renderSection = [&](const std::string& id) {
const auto g = repo.loadGrid(id);
if (is2D) {
auto line = geopro::render::buildSurveyLine(g, *frame);
if (line) scene->addActor(line);
} else if (*showCurtain) {
const auto cs = repo.loadColorScale(id);
auto curtain = geopro::render::buildCurtain(g, cs, *frame);
if (curtain) {
curtain->SetScale(1.0, 1.0, kCurtainZScale); // 纵向夸张成墙
scene->addActor(curtain);
}
}
// 中央编排已解耦到 CentralScene::rebuildCentralScene数据驱动。本轮空 sections → 空背景占位。
// 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, showCurtain, frame]() {
geopro::app::rebuildCentralScene(*scene, rendererPtr, renderWindowPtr, *viewMode,
std::vector<geopro::app::SectionInput>{}, *showCurtain,
*frame, kCurtainZScale);
};
// 遍历对象树收集所有勾选的测线(TM),渲染其 dd_section 数据集(可多条共存)。
QList<QTreeWidgetItem*> stack;
for (int i = 0; i < tree->topLevelItemCount(); ++i) stack.append(tree->topLevelItem(i));
while (!stack.isEmpty()) {
QTreeWidgetItem* cur = stack.takeFirst();
for (int i = 0; i < cur->childCount(); ++i) stack.append(cur->child(i));
const QString tmId = cur->data(0, kRoleTmId).toString();
if (tmId.isEmpty()) continue; // GS 节点忽略
if (cur->checkState(0) != Qt::Checked) continue; // 仅显示勾选的测线
const auto* tm = findTm(*structure, tmId.toStdString());
if (!tm) continue;
for (const auto& ds : tm->dss)
if (ds.ddType == "dd_section") renderSection(ds.id);
}
// 三维「体素 / 切片」图层:两交叉测线散点经 CRS 配准 IDW 成体素。
// 体素=GPU 体绘制(与帘面同纵向夸张);切片=vtkImagePlaneWidget 在体素 image 上交互拖切面。
// 注:切片 widget 作用于 image 原始米坐标(无 actor 夸张),与夸张后的体绘制存在纵向比例差
// (spec M-3 Z 基准统一待办);切片本身演示 dd_slice 交互正确。
if (!is2D && (*showVoxel || *showSlice) && crs) {
const auto profs = repo.loadVoxelScatters();
const auto vcs = repo.loadScatterColorScale("grid1");
// 纵向夸张烤进 image(zDisplayScale=kCurtainZScale),使体绘制/切片/帘面纵向一致。
auto vr = geopro::render::buildVoxelFromScatters(profs, vcs, *crs, *frame, 1.0, 0.5, 2.0,
4.0, kCurtainZScale);
if (vr.valid()) {
if (*showVoxel) {
rendererPtr->AddVolume(vr.volume); // 夸张已烤进 image无需 actor SetScale
}
vtkRenderWindowInteractor* interactor = renderWindowPtr->GetInteractor();
if (*showSlice && interactor) {
const std::vector<double> stops = vcs.stopValues();
const double vmn = stops.size() >= 2 ? stops.front() : 0.0;
const double vmx = stops.size() >= 2 ? stops.back() : 1.0;
auto lut = geopro::render::buildLut(vcs, vmn, vmx, 256);
int dims[3] = {1, 1, 1};
vr.image->GetDimensions(dims);
auto plane = vtkSmartPointer<vtkImagePlaneWidget>::New();
plane->SetInteractor(interactor);
plane->SetInputData(vr.image);
plane->SetPlaneOrientationToXAxes();
plane->SetSliceIndex(dims[0] / 2);
plane->SetLookupTable(lut);
plane->DisplayTextOn();
// 左键拖动=移动切面(默认左键是取值光标十字,不直观);中键仍可取值。
plane->SetLeftButtonAction(vtkImagePlaneWidget::VTK_SLICE_MOTION_ACTION);
plane->SetMiddleButtonAction(vtkImagePlaneWidget::VTK_CURSOR_ACTION);
plane->On();
*slicePlane = plane;
}
}
}
// 三维「地形」图层GDAL 读 DEM(高程)+影像(EPSG:3857)重投影到世界系warp 面 + 纹理。
if (!is2D && *showTerrain && crs) {
// zOffset=refElev 使地形落在测线地表高程附近(不按绝对高程浮空);zScale=1 真实起伏。
auto terr = geopro::render::buildTerrain(repo.demPath(), repo.imagePath(), *frame,
refElev, 1.0);
if (terr) scene->addActor(terr);
}
if (is2D)
geopro::render::applyTop2D(rendererPtr);
else
geopro::render::applyFree3D(rendererPtr);
rendererPtr->ResetCamera();
renderWindowPtr->Render();
};
// 勾选/取消某测线(TM) → 重建当前视图内容(勾的才显示;可多条共存)。
QObject::connect(tree, &QTreeWidget::itemChanged, tree,
[rebuildCentral](QTreeWidgetItem* item, int) {
if (item->data(0, kRoleTmId).toString().isEmpty()) return; // GS 忽略
rebuildCentral();
});
// 单击测线(TM) → 左下数据列表填充其采集批次(数据集) + 动态标题 + 数据 Tab 数量。
QObject::connect(tree, &QTreeWidget::itemClicked, tree,
[structure, datasetList, datasetTitle, datasetTabs](QTreeWidgetItem* item, int) {
const QString tmId = item->data(0, kRoleTmId).toString();
if (tmId.isEmpty()) return; // GS 节点无数据集
const auto* tm = findTm(*structure, tmId.toStdString());
if (!tm) return;
geopro::app::populateDatasetList(datasetList, tm->dss);
if (datasetTitle)
datasetTitle->setText(QStringLiteral("数据集显示栏 · %1").arg(item->text(0)));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(tm->dss.size())));
});
// ── 数据详情共享状态 + 重建 ──────────────────────────────────────────
// 当前选中数据集 id空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
auto currentDsId = std::make_shared<QString>();
@ -656,15 +499,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
.arg(anomalies.size()));
};
// ── 单击左下数据列表的采集批次(DS) → 加载到数据详情/异常/属性 ──
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
[loadDataset](QListWidgetItem* item) {
const QString dsId = item->data(geopro::app::kDsIdRole).toString();
const QString ddType = item->data(geopro::app::kDsDdTypeRole).toString();
if (ddType != "dd_section") return; // 仅剖面网格有详情图
[propLabel, detailRendererPtr, detailRenderWindowPtr](QListWidgetItem* item) {
const QString name =
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
loadDataset(dsId, name);
detailRendererPtr->RemoveAllViewProps();
detailRenderWindowPtr->Render();
propLabel->setText(QStringLiteral(
"数据集: %1\n(该数据集的剖面/反演渲染将在下一阶段接入 dd 接口)").arg(name));
});
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
@ -755,45 +598,72 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
rebuildCentral();
});
// ── 启动默认:测线已勾选,但 itemChanged 在 connect 之前触发故未渲染;这里重建一次中央内容
// ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)
rebuildCentral();
// 启动默认:选第一个含 dd_section 的测线 → 填充数据列表 + 加载其首个 dd_section 详情(对齐原型)。
for (const auto& gs : *structure) {
const geopro::data::TmNode* picked = nullptr;
for (const auto& tm : gs.tms) {
const bool hasSection =
std::any_of(tm.dss.begin(), tm.dss.end(),
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
if (hasSection) { picked = &tm; break; }
}
if (!picked) continue;
geopro::app::populateDatasetList(datasetList, picked->dss);
if (datasetTitle)
datasetTitle->setText(
QStringLiteral("数据集显示栏 · %1").arg(QString::fromStdString(picked->name)));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(picked->dss.size())));
for (const auto& ds : picked->dss)
if (ds.ddType == "dd_section") {
loadDataset(QString::fromStdString(ds.id), QString::fromStdString(ds.name));
break;
}
break;
}
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
geopro::app::TopBar* topBar = nullptr;
{
auto* topChrome = new QWidget(&window);
auto* topLayout = new QVBoxLayout(topChrome);
topLayout->setContentsMargins(0, 0, 0, 0);
topLayout->setSpacing(0);
topLayout->addWidget(geopro::app::buildMenuBar(topChrome));
topLayout->addWidget(new geopro::app::TopBar(topChrome));
topBar = new geopro::app::TopBar(topChrome);
topLayout->addWidget(topBar);
window.setMenuWidget(topChrome);
}
// ── 控制器 ↔ UI 信号接线(导航壳)──────────────────────────────────────
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchWorkspace);
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchProject);
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::tmClicked, &nav,
&geopro::controller::WorkbenchNavController::selectTm);
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::workspacesLoaded, topBar,
[topBar](const std::vector<geopro::data::Workspace>& list, const QString& cur) {
topBar->setWorkspaces(list, cur);
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::projectsLoaded, topBar,
[topBar](const std::vector<geopro::data::ProjectSummary>& list,
const QString& cur) { topBar->setProjects(list, cur); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
[objectTree, datasetList, datasetTitle, datasetTabs](
const QString& projectName,
const std::vector<geopro::data::StructNode>& nodes) {
objectTree->setStructure(projectName, nodes);
datasetList->clear(); // 切项目清空 DS 列表
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(0, QStringLiteral("数据"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsNode>& list) {
geopro::app::populateDatasetList(datasetList, list);
if (datasetTitle)
datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(
0, QStringLiteral("数据 (%1)").arg(static_cast<int>(list.size())));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree,
[objectTree, &window](const QString& stage, const QString& msg) {
if (stage == QStringLiteral("structure") ||
stage == QStringLiteral("projects"))
objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg));
window.statusBar()->showMessage(
QStringLiteral("加载失败(%1%2").arg(stage, msg), 8000);
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window,
[](bool busy) {
if (busy)
QApplication::setOverrideCursor(Qt::WaitCursor);
else
QApplication::restoreOverrideCursor();
});
// 底部状态栏常驻显示坐标系与世界系原点wayfinding用户随时知道当前空间基准
window.statusBar()->showMessage(
QStringLiteral("就绪 | 坐标系 %1 | 世界系原点 %2, %3")
@ -851,13 +721,19 @@ int main(int argc, char* argv[])
geopro::data::LocalSampleRepository repo(
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
// 导航仓储 + 控制器(接口/逻辑层):用同一共享会话 ApiClient。
geopro::data::ApiProjectRepository projectRepo(api);
geopro::controller::WorkbenchNavController nav(projectRepo);
QMainWindow window;
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
window.resize(1280, 800);
window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸
buildWorkbench(window, repo);
buildWorkbench(window, repo, nav);
window.show();
nav.start(); // 进入工作台后拉真实 空间/项目/结构
return app.exec();
}