From 405fb2ae4ff42f471e1bb2b59ef85f2ac2a8e859 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 9 Jun 2026 12:01:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E7=9C=9F=E5=AE=9E=E5=AF=BC=E8=88=AA=EF=BC=88?= =?UTF-8?q?=E7=A9=BA=E9=97=B4/=E9=A1=B9=E7=9B=AE/=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E6=A0=91/DS=EF=BC=89=EF=BC=8C=E4=B8=AD=E5=A4=AE=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=8D=A0=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/main.cpp | 300 ++++++++++++++--------------------------------- 1 file changed, 88 insertions(+), 212 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 2fee684..0e8156a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -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; - -// 从对象结构树构建 QTreeWidget:GS → TM 两层(对齐原型;DS=采集批次在左下「数据列表」,不进树)。 -// TM(测线) 项可勾选(复选框):勾选驱动该测线的 dd_section 在中央场景显示;UserRole+2 存 tmId。 -// 含 dd_section 的测线默认勾选,启动即显示。 -void populateTree(QTreeWidget* tree, const std::vector& 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& 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 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>(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); - } - } - }; - - // 遍历对象树收集所有勾选的测线(TM),渲染其 dd_section 数据集(可多条共存)。 - QList 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 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::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(); + // 中央编排已解耦到 CentralScene::rebuildCentralScene(数据驱动)。本轮空 sections → 空背景占位。 + // 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。 + auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, showCurtain, frame]() { + geopro::app::rebuildCentralScene(*scene, rendererPtr, renderWindowPtr, *viewMode, + std::vector{}, *showCurtain, + *frame, kCurtainZScale); }; - // 勾选/取消某测线(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(tm->dss.size()))); - }); - // ── 数据详情共享状态 + 重建 ────────────────────────────────────────── // 当前选中数据集 id(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。 auto currentDsId = std::make_shared(); @@ -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(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& list, const QString& cur) { + topBar->setWorkspaces(list, cur); + }); + QObject::connect(&nav, &geopro::controller::WorkbenchNavController::projectsLoaded, topBar, + [topBar](const std::vector& 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& 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& list) { + geopro::app::populateDatasetList(datasetList, list); + if (datasetTitle) + datasetTitle->setText(QStringLiteral("数据集显示栏")); + datasetTabs->setTabText( + 0, QStringLiteral("数据 (%1)").arg(static_cast(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(); }