1147 lines
61 KiB
C++
1147 lines
61 KiB
C++
// M1 工作台(视图重构 Task B):正确产品模型。
|
||
// - 左上 对象显示栏:GS→TM(测线,复选框)。勾选测线 → 在中央显示其 dd_section,可多条共存。
|
||
// - 左下 数据真实显示栏:单击测线 → 列其采集批次(数据集,tab 数据/文件)。单击采集批次 → 数据详情+异常+属性。
|
||
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
|
||
// 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。
|
||
// 三维视图 = 勾选测线的 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。
|
||
// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)
|
||
// / 切片(dd_slice,vtkImagePlaneWidget 在体素 image 上交互拖切面) / 地形(DEM 高程面 + 影像纹理)。
|
||
// 切视图 / 勾选变化 / 图层变化 → 重建对应内容。
|
||
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
|
||
// 单击某 DS → 显示该数据集:
|
||
// 网格数据 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
|
||
// 原数据 = #17 彩色散点(buildScatter,x=距离/y=深度,按散点自带色阶上色)。
|
||
// 显示异常 = 在上图叠加异常圈定(buildAnomalies,dashed 折线,同纵向夸张对齐)。
|
||
// 两者皆平躺俯视正交 + 属性。
|
||
// - 右 属性:选中数据集属性文本。
|
||
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame(全项目共享,保证多视图配准)。
|
||
|
||
#include <fstream>
|
||
#include <initializer_list>
|
||
#include <memory>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include <QActionGroup>
|
||
#include <QApplication>
|
||
#include <QCheckBox>
|
||
#include <QColor>
|
||
#include <QDialog>
|
||
#include <QEasingCurve>
|
||
#include <QEvent>
|
||
#include <QFile>
|
||
#include <QButtonGroup>
|
||
#include <QFrame>
|
||
#include <QHBoxLayout>
|
||
#include <QGraphicsOpacityEffect>
|
||
#include <QLabel>
|
||
#include <QListWidget>
|
||
#include <QListWidgetItem>
|
||
#include <QToolButton>
|
||
#include <QKeySequence>
|
||
#include <QProcess>
|
||
#include <QSettings>
|
||
#include <QShortcut>
|
||
#include <QSignalBlocker>
|
||
#include <QPropertyAnimation>
|
||
#include <QVariantAnimation>
|
||
#include <QStringList>
|
||
#include <QTabWidget>
|
||
#include <QMainWindow>
|
||
#include <QStatusBar>
|
||
#include <QStyle>
|
||
#include <QSurfaceFormat>
|
||
#include <QTimer>
|
||
#include <QToolBar>
|
||
#include <QTreeWidget>
|
||
#include <QTreeWidgetItem>
|
||
#include <QVBoxLayout>
|
||
#include <QWidget>
|
||
|
||
#include <DockAreaTitleBar.h>
|
||
#include <DockAreaWidget.h>
|
||
#include <DockManager.h>
|
||
#include <DockWidget.h>
|
||
|
||
#include <ElaApplication.h>
|
||
#include <ElaCheckBox.h>
|
||
#include <ElaDef.h>
|
||
#include <ElaTheme.h>
|
||
#include <ElaToolButton.h>
|
||
#include <ElaWindow.h>
|
||
|
||
#include "model/ColorScale.hpp"
|
||
#include "model/Field.hpp"
|
||
#include "repo/LocalSampleRepository.hpp"
|
||
|
||
#include "ApiClient.hpp"
|
||
#include "AuthService.hpp"
|
||
#include "Credential.hpp"
|
||
#include "Glyphs.hpp"
|
||
#include "PanelHeader.hpp"
|
||
#include "Theme.hpp"
|
||
#include "TopBar.hpp"
|
||
#include "CentralScene.hpp"
|
||
#include "ProjectListDialog.hpp"
|
||
#include "WorkbenchNavController.hpp"
|
||
#include "api/ApiProjectRepository.hpp"
|
||
#include "panels/ObjectTreePanel.hpp"
|
||
#include "login/LoginWindow.hpp"
|
||
#include "panels/AnomalyListPanel.hpp"
|
||
#include "panels/DatasetListPanel.hpp"
|
||
|
||
#include "CameraPreset.hpp"
|
||
#include "ColorLutBuilder.hpp"
|
||
#include "Scene.hpp"
|
||
#include "VoxelFromScatters.hpp"
|
||
#include "actors/AnomalyActor.hpp"
|
||
#include "actors/CurtainActor.hpp"
|
||
#include "actors/ElectrodeActor.hpp"
|
||
#include "actors/GridContourActor.hpp"
|
||
#include "actors/MapLineActor.hpp"
|
||
#include "actors/ScatterActor.hpp"
|
||
#include "actors/TerrainActor.hpp"
|
||
|
||
#include "geo/CrsTransform.hpp"
|
||
#include "geo/GeoLocalFrame.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <exception>
|
||
#include <memory>
|
||
#include <set>
|
||
#include <vector>
|
||
|
||
#include <QVTKOpenGLStereoWidget.h>
|
||
#include <vtkActor.h>
|
||
#include <vtkCamera.h>
|
||
#include <vtkCameraInterpolator.h>
|
||
#include <vtkGenericOpenGLRenderWindow.h>
|
||
#include <vtkImagePlaneWidget.h>
|
||
#include <vtkLookupTable.h>
|
||
#include <vtkProperty.h>
|
||
#include <vtkRenderWindowInteractor.h>
|
||
#include <vtkRenderer.h>
|
||
#include <vtkSmartPointer.h>
|
||
|
||
namespace {
|
||
|
||
// 居中浮层定位器:监视 host(中央 QVTK)尺寸/显示变化,把 overlay 浮层
|
||
// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。
|
||
// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。
|
||
class CenterOverlay : public QObject {
|
||
public:
|
||
CenterOverlay(QWidget* overlay, QWidget* host)
|
||
: QObject(host), overlay_(overlay), host_(host)
|
||
{
|
||
host_->installEventFilter(this);
|
||
}
|
||
void reposition()
|
||
{
|
||
overlay_->adjustSize();
|
||
const QSize h = host_->size();
|
||
const QSize o = overlay_->size();
|
||
overlay_->move(host_->x() + (h.width() - o.width()) / 2,
|
||
host_->y() + (h.height() - o.height()) / 2);
|
||
overlay_->raise();
|
||
}
|
||
|
||
protected:
|
||
bool eventFilter(QObject* obj, QEvent* e) override
|
||
{
|
||
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show))
|
||
reposition();
|
||
return QObject::eventFilter(obj, e);
|
||
}
|
||
|
||
private:
|
||
QWidget* overlay_;
|
||
QWidget* host_;
|
||
};
|
||
|
||
// 相机补间 + actor 淡入:从 from 位姿平滑过渡到 to 位姿,同时 actors 透明度 0→1。
|
||
// vtkCameraInterpolator 两关键帧线性插值(缓动交给 QEasingCurve),单条 QVariantAnimation
|
||
// 逐帧驱动并 Render;结束回调锁定到目标态(防插值末值误差/残留半透明)。
|
||
// 渐进增强:动效只是过渡,最终一帧永远是正确的目标态,故即使观感不佳也不破坏功能。
|
||
void animateReveal(vtkRenderer* renderer, vtkGenericOpenGLRenderWindow* rw,
|
||
vtkSmartPointer<vtkCamera> fromCam, vtkSmartPointer<vtkCamera> toCam,
|
||
std::vector<vtkSmartPointer<vtkActor>> actors, int durationMs, QObject* owner)
|
||
{
|
||
auto interp = vtkSmartPointer<vtkCameraInterpolator>::New();
|
||
interp->SetInterpolationTypeToLinear();
|
||
interp->AddCamera(0.0, fromCam);
|
||
interp->AddCamera(1.0, toCam);
|
||
|
||
auto* anim = new QVariantAnimation(owner);
|
||
anim->setDuration(durationMs);
|
||
anim->setStartValue(0.0);
|
||
anim->setEndValue(1.0);
|
||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||
QObject::connect(anim, &QVariantAnimation::valueChanged, owner,
|
||
[interp, renderer, rw, actors](const QVariant& v) {
|
||
const double t = v.toDouble();
|
||
interp->InterpolateCamera(t, renderer->GetActiveCamera());
|
||
for (const auto& a : actors)
|
||
if (a) a->GetProperty()->SetOpacity(t);
|
||
renderer->ResetCameraClippingRange();
|
||
rw->Render();
|
||
});
|
||
QObject::connect(anim, &QVariantAnimation::finished, owner,
|
||
[renderer, rw, actors, toCam]() {
|
||
renderer->GetActiveCamera()->DeepCopy(toCam);
|
||
for (const auto& a : actors)
|
||
if (a) a->GetProperty()->SetOpacity(1.0);
|
||
renderer->ResetCameraClippingRange();
|
||
rw->Render();
|
||
});
|
||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||
}
|
||
|
||
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
||
std::string readPem(const std::string& path)
|
||
{
|
||
std::ifstream in(path, std::ios::binary);
|
||
if (!in) return {};
|
||
std::ostringstream ss;
|
||
ss << in.rdbuf();
|
||
return ss.str();
|
||
}
|
||
|
||
// 取 vector 中位数(用于由测线 lat/lon 推世界系原点)。空则返回 0。
|
||
double median(std::vector<double> v)
|
||
{
|
||
if (v.empty()) return 0.0;
|
||
std::sort(v.begin(), v.end());
|
||
const size_t n = v.size();
|
||
return n % 2 ? v[n / 2] : 0.5 * (v[n / 2 - 1] + v[n / 2]);
|
||
}
|
||
|
||
// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。
|
||
using geopro::app::ViewMode;
|
||
|
||
// 数据详情显示内容(默认网格数据)。网格数据=#18 banded;原数据=#17 散点(对齐原型命名)。
|
||
enum class DetailMode { Section18, Scatter17 };
|
||
|
||
// #17 散点屏幕像素方块边长。
|
||
constexpr float kScatterPointSize = 4.0F;
|
||
|
||
// 纵向夸张倍数(Z 基准统一,M-3):全项目共用同一倍数,使 帘面(z) / 体素 / 切片 /
|
||
// 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。
|
||
// 单一可调常量:要整体调纵向观感改这一处即可。
|
||
constexpr double kVerticalExaggeration = 2.0;
|
||
|
||
// 项目 CRS(实证确定,STATUS §4):散点 projX/Y→经纬→GeoLocalFrame 配准体素到世界系。
|
||
constexpr const char* kProjectCrs = "EPSG:4547"; // CGCS2000 / 3-degree GK CM 114E
|
||
constexpr const char* kWgs84 = "EPSG:4326";
|
||
|
||
// 在给定 QMainWindow 上构建 M1 工作台。
|
||
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
|
||
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo,
|
||
geopro::data::IProjectRepository& projectRepo,
|
||
geopro::controller::WorkbenchNavController& nav)
|
||
{
|
||
// ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ──
|
||
// 全项目共享(shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
|
||
const auto baseGrid = repo.loadGrid("grid1");
|
||
const double lat0 = median(baseGrid.lat);
|
||
const double lon0 = median(baseGrid.lon);
|
||
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(lat0, lon0);
|
||
// 测线地表高程基准(地形 z rebase 用,使地形落在测线附近而非按绝对高程浮空)。
|
||
const double refElev = baseGrid.elevation.empty() ? 0.0 : median(baseGrid.elevation);
|
||
|
||
// ── 中央 QVTK + Scene(竖直帘面场景)─────────────────────────────────
|
||
// Scene 非 QObject:堆分配,用 widget 销毁信号清理(widget 随 window 销毁)。
|
||
auto* scene = new geopro::render::Scene();
|
||
auto* vtkWidget = new QVTKOpenGLStereoWidget();
|
||
QObject::connect(vtkWidget, &QObject::destroyed, [scene]() { delete scene; });
|
||
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
|
||
vtkWidget->setRenderWindow(renderWindow);
|
||
renderWindow->AddRenderer(scene->renderer());
|
||
|
||
vtkRenderer* rendererPtr = scene->renderer();
|
||
vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get();
|
||
|
||
// 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。
|
||
auto viewMode = std::make_shared<ViewMode>(ViewMode::Map2D);
|
||
|
||
// 三维图层显隐(由「视图详情」浮层控制)+ 项目 CRS→WGS84(体素配准)。
|
||
auto showCurtain = std::make_shared<bool>(true); // 帘面,默认显示
|
||
auto showVoxel = std::make_shared<bool>(false); // 体素,默认关
|
||
auto showTerrain = std::make_shared<bool>(false); // 地形(DEM+影像),默认关
|
||
auto showSlice = std::make_shared<bool>(false); // dd_slice 交互切片,默认关
|
||
// 持久的切片 widget(挂 interactor,跨重建保活;rebuildCentral 据条件创建/拆除)。
|
||
auto slicePlane = std::make_shared<vtkSmartPointer<vtkImagePlaneWidget>>();
|
||
std::shared_ptr<geopro::core::CrsTransform> crs; // PROJ 失败→空→体素层无效(不崩)
|
||
try {
|
||
crs = std::make_shared<geopro::core::CrsTransform>(kProjectCrs, kWgs84);
|
||
} catch (const std::exception&) {
|
||
crs.reset();
|
||
}
|
||
|
||
// 停靠系统配置(必须在 CDockManager 构造前设置):对齐原型——面板固定、
|
||
// 标题栏不显示「关闭 / 浮动 / 标签菜单」等子窗口操作按钮,并关闭自动隐藏(钉住)。
|
||
ads::CDockManager::setConfigFlags(ads::CDockManager::DefaultOpaqueConfig);
|
||
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasCloseButton, false);
|
||
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasUndockButton, false);
|
||
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasTabsMenuButton, false);
|
||
ads::CDockManager::setConfigFlag(ads::CDockManager::ActiveTabHasCloseButton, false);
|
||
ads::CDockManager::setConfigFlag(ads::CDockManager::AlwaysShowTabs, true); // 单面板也显示标题头
|
||
ads::CDockManager::setConfigFlag(ads::CDockManager::FocusHighlighting, false);
|
||
ads::CDockManager::setAutoHideConfigFlags(ads::CDockManager::AutoHideFlags()); // 禁用自动隐藏
|
||
|
||
auto* dockManager = new ads::CDockManager(&window);
|
||
window.setCentralWidget(dockManager);
|
||
|
||
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
|
||
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
|
||
// 捕获 ADS 基样式一次(避免每次切换重复追加而无限增长),切主题时用 base + 重新着色的覆盖。
|
||
const QString dockBaseQss = dockManager->styleSheet();
|
||
auto applyDockSplitter = [dockManager, dockBaseQss]() {
|
||
dockManager->setStyleSheet(
|
||
dockBaseQss +
|
||
geopro::app::themed(QStringLiteral(
|
||
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }"
|
||
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }")));
|
||
};
|
||
applyDockSplitter();
|
||
QObject::connect(eTheme, &ElaTheme::themeModeChanged, dockManager,
|
||
[applyDockSplitter](ElaThemeType::ThemeMode) { applyDockSplitter(); });
|
||
|
||
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮),ADS 自带标题栏随后隐藏,
|
||
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
|
||
auto wrapWithHeader = [](geopro::app::Glyph icon, const QString& title, QWidget* content,
|
||
const QVector<geopro::app::HeaderAction>& actions = {}) {
|
||
auto* box = new QWidget();
|
||
auto* v = new QVBoxLayout(box);
|
||
v->setContentsMargins(0, 0, 0, 0);
|
||
v->setSpacing(0);
|
||
v->addWidget(geopro::app::buildPanelHeader(icon, title, actions));
|
||
v->addWidget(content, 1);
|
||
return box;
|
||
};
|
||
|
||
// 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。
|
||
auto* centerWidget = new QWidget();
|
||
auto* centerLayout = new QVBoxLayout(centerWidget);
|
||
centerLayout->setContentsMargins(0, 0, 0, 0);
|
||
centerLayout->setSpacing(0);
|
||
|
||
// 分段工具条按钮样式(QToolButton + 主题化 QSS):选中=强调色文字 + 强调色下划线,明暗都清晰。
|
||
// ElaToolButton 选中只画极淡 BasicHover、且不可经 QSS 改,故这类需清晰选中态的用 QToolButton。
|
||
const QString kBarBtnQss =
|
||
QStringLiteral(
|
||
"QToolButton{ border:none; border-radius:6px; padding:6px 12px; color:#1F2A3D;"
|
||
" font-size:%1px; }"
|
||
"QToolButton:hover{ background:#EEF3FB; }"
|
||
"QToolButton:checked{ color:#2D6CB5; font-weight:%2;"
|
||
" border-bottom:2px solid #2D6CB5; }")
|
||
.arg(geopro::app::type::kBody)
|
||
.arg(geopro::app::type::kWeightSemibold);
|
||
|
||
// 工具条:「二维地图/三维视图」两个互斥可勾选按钮。默认二维地图。
|
||
auto* viewToolBar = new QWidget();
|
||
auto* viewBarLay = new QHBoxLayout(viewToolBar);
|
||
viewBarLay->setContentsMargins(8, 6, 8, 6);
|
||
viewBarLay->setSpacing(6);
|
||
auto* viewGroup = new QButtonGroup(viewToolBar);
|
||
viewGroup->setExclusive(true);
|
||
auto* act2D = new QToolButton(viewToolBar);
|
||
act2D->setText(QStringLiteral("二维地图"));
|
||
act2D->setCheckable(true);
|
||
auto* act3D = new QToolButton(viewToolBar);
|
||
act3D->setText(QStringLiteral("三维视图"));
|
||
act3D->setCheckable(true);
|
||
viewGroup->addButton(act2D);
|
||
viewGroup->addButton(act3D);
|
||
viewBarLay->addWidget(act2D);
|
||
viewBarLay->addWidget(act3D);
|
||
viewBarLay->addStretch();
|
||
act2D->setChecked(true); // 默认二维地图
|
||
geopro::app::applyThemedStyleSheet(viewToolBar, kBarBtnQss);
|
||
centerLayout->addWidget(viewToolBar);
|
||
centerLayout->addWidget(vtkWidget, 1);
|
||
|
||
// ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。
|
||
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。
|
||
auto* layerPanel = new QFrame(centerWidget);
|
||
layerPanel->setFrameShape(QFrame::StyledPanel);
|
||
geopro::app::applyThemedStyleSheet(
|
||
layerPanel,
|
||
QStringLiteral("QFrame{background:#FFFFFF;border:1px solid #D5DBE5;border-radius:8px;}"
|
||
"QCheckBox{padding:2px 1px;color:#1F2A3D;}"
|
||
"QCheckBox:disabled{color:#9AA6B6;}"));
|
||
auto* layerLayout = new QVBoxLayout(layerPanel);
|
||
// 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。
|
||
layerLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMl,
|
||
geopro::app::space::kLg, geopro::app::space::kMl);
|
||
layerLayout->setSpacing(geopro::app::space::kSm);
|
||
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
|
||
geopro::app::applyThemedStyleSheet(
|
||
layerTitle, QStringLiteral("font-weight:%1;color:#2D6CB5;border:none;background:transparent;"
|
||
"padding-bottom:3px;font-size:%2px;")
|
||
.arg(geopro::app::type::kWeightSemibold)
|
||
.arg(geopro::app::type::kTitle));
|
||
auto* chkCurtain = new ElaCheckBox(QStringLiteral("帘面(断面墙)"));
|
||
chkCurtain->setChecked(true);
|
||
auto* chkVoxel = new ElaCheckBox(QStringLiteral("体素(dd_voxel)"));
|
||
chkVoxel->setChecked(false);
|
||
auto* chkTerrain = new ElaCheckBox(QStringLiteral("地形(DEM+影像)"));
|
||
chkTerrain->setChecked(false);
|
||
auto* chkSlice = new ElaCheckBox(QStringLiteral("切片(dd_slice)"));
|
||
chkSlice->setChecked(false);
|
||
if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示
|
||
const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用");
|
||
chkVoxel->setEnabled(false); chkVoxel->setToolTip(tip);
|
||
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);
|
||
layerLayout->addWidget(chkSlice);
|
||
layerLayout->addWidget(chkTerrain);
|
||
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
||
|
||
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
|
||
// 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中;
|
||
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
|
||
auto* emptyState = new QFrame(centerWidget);
|
||
emptyState->setObjectName(QStringLiteral("centralEmpty"));
|
||
emptyState->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||
emptyState->setStyleSheet(QStringLiteral(
|
||
"#centralEmpty { background: transparent; }"
|
||
"#centralEmpty QLabel { background: transparent; }"));
|
||
auto* esLay = new QVBoxLayout(emptyState);
|
||
esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl,
|
||
geopro::app::space::kXl, geopro::app::space::kXl);
|
||
esLay->setSpacing(geopro::app::space::kMd);
|
||
esLay->setAlignment(Qt::AlignCenter);
|
||
|
||
auto* esIcon = new QLabel(emptyState);
|
||
esIcon->setPixmap(
|
||
geopro::app::makeGlyph(geopro::app::Glyph::Dataset, QColor("#C2CCDA"), 56).pixmap(56, 56));
|
||
esIcon->setAlignment(Qt::AlignCenter);
|
||
|
||
auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState);
|
||
esTitle->setAlignment(Qt::AlignCenter);
|
||
geopro::app::applyThemedStyleSheet(
|
||
esTitle, QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;")
|
||
.arg(geopro::app::type::kHeading)
|
||
.arg(geopro::app::type::kWeightSemibold));
|
||
|
||
auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n"
|
||
"切到「三维视图」可叠加帘面、体素与地形图层"),
|
||
emptyState);
|
||
esHint->setAlignment(Qt::AlignCenter);
|
||
geopro::app::applyThemedStyleSheet(
|
||
esHint, QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody));
|
||
|
||
esLay->addWidget(esIcon);
|
||
esLay->addWidget(esTitle);
|
||
esLay->addWidget(esHint);
|
||
|
||
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
|
||
emptyCentering->reposition();
|
||
|
||
// 引导层淡入(350ms,仅透明度,OutCubic):首屏空态出现的克制过渡,不阻塞任务。
|
||
auto* esFx = new QGraphicsOpacityEffect(emptyState);
|
||
emptyState->setGraphicsEffect(esFx);
|
||
auto* esAnim = new QPropertyAnimation(esFx, "opacity", emptyState);
|
||
esAnim->setDuration(350);
|
||
esAnim->setStartValue(0.0);
|
||
esAnim->setEndValue(1.0);
|
||
esAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||
esAnim->start(QAbstractAnimation::DeleteWhenStopped);
|
||
|
||
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
||
vtkDock->setWidget(centerWidget);
|
||
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
||
|
||
// ── 下方「数据详情」dock:独立 QVTK 小视图(独立 renderer/renderWindow)──
|
||
// 单击 DS → 显示该数据集平面反演剖面(#18 banded,平躺俯视正交)。
|
||
auto* detailWidget = new QVTKOpenGLStereoWidget();
|
||
vtkNew<vtkGenericOpenGLRenderWindow> detailRenderWindow;
|
||
vtkNew<vtkRenderer> detailRenderer;
|
||
{
|
||
double r, g, b;
|
||
geopro::app::vtkBackground(r, g, b); // 背景随主题
|
||
detailRenderer->SetBackground(r, g, b);
|
||
}
|
||
detailWidget->setRenderWindow(detailRenderWindow);
|
||
detailRenderWindow->AddRenderer(detailRenderer);
|
||
vtkRenderer* detailRendererPtr = detailRenderer.Get();
|
||
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
|
||
// 注:VTK 背景随主题切换的连接放在 rebuildCentral/rebuildDetail 定义之后(直接重跑它们,
|
||
// 走完整渲染路径必重绘,比手动 SetBackground+Render 稳)。
|
||
|
||
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
|
||
auto* detailContainer = new QWidget();
|
||
auto* detailLayout = new QVBoxLayout(detailContainer);
|
||
detailLayout->setContentsMargins(0, 0, 0, 0);
|
||
detailLayout->setSpacing(0);
|
||
|
||
// 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常/电极/等值线」开关。
|
||
// QToolButton + 主题化 QSS(选中=强调色文字+下划线,明暗都清晰)。
|
||
auto* detailToolBar = new QWidget();
|
||
auto* detailBarLay = new QHBoxLayout(detailToolBar);
|
||
detailBarLay->setContentsMargins(8, 6, 8, 6);
|
||
detailBarLay->setSpacing(6);
|
||
auto makeBarBtn = [detailToolBar](const QString& text, bool checkable) {
|
||
auto* b = new QToolButton(detailToolBar);
|
||
b->setText(text);
|
||
b->setCheckable(checkable);
|
||
return b;
|
||
};
|
||
auto* detailGroup = new QButtonGroup(detailToolBar);
|
||
detailGroup->setExclusive(true);
|
||
auto* actScatter = makeBarBtn(QStringLiteral("原数据"), true);
|
||
auto* actSection = makeBarBtn(QStringLiteral("网格数据"), true);
|
||
detailGroup->addButton(actScatter);
|
||
detailGroup->addButton(actSection);
|
||
detailBarLay->addWidget(actScatter);
|
||
detailBarLay->addWidget(actSection);
|
||
actSection->setChecked(true); // 默认网格数据 (#18)
|
||
auto* barSep = new QFrame(detailToolBar);
|
||
barSep->setFrameShape(QFrame::VLine);
|
||
barSep->setObjectName(QStringLiteral("topDivider"));
|
||
detailBarLay->addSpacing(4);
|
||
detailBarLay->addWidget(barSep);
|
||
detailBarLay->addSpacing(4);
|
||
auto* actShowAnomaly = makeBarBtn(QStringLiteral("显示异常"), true);
|
||
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
|
||
auto* actShowElectrodes = makeBarBtn(QStringLiteral("显示电极"), true);
|
||
actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型)
|
||
auto* actShowContour = makeBarBtn(QStringLiteral("显示等值线"), true);
|
||
actShowContour->setChecked(true); // 默认显示等值线(对齐原型)
|
||
detailBarLay->addWidget(actShowAnomaly);
|
||
detailBarLay->addWidget(actShowElectrodes);
|
||
detailBarLay->addWidget(actShowContour);
|
||
detailBarLay->addStretch();
|
||
geopro::app::applyThemedStyleSheet(detailToolBar, kBarBtnQss);
|
||
detailLayout->addWidget(detailToolBar);
|
||
detailLayout->addWidget(detailWidget, 1);
|
||
|
||
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
||
detailDock->setWidget(wrapWithHeader(
|
||
geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailContainer,
|
||
{{geopro::app::Glyph::Collapse, QStringLiteral("折叠")},
|
||
{geopro::app::Glyph::Download, QStringLiteral("导出")}}));
|
||
// 放在中央视图下方。
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
|
||
|
||
// 左上 dock:对象树(真实结构:项目根 → GS → TM)。被动视图,数据由控制器推送。
|
||
auto* objectTree = new geopro::app::ObjectTreePanel();
|
||
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
|
||
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"),
|
||
objectTree,
|
||
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
|
||
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
||
|
||
// 列表选中色:写死的强调蓝(明 #C2D9F2 / 暗 #33527A)+ 适配文字,:!active 防失焦变淡;
|
||
// 与对象树选中色一致。本地 QSS 覆盖全局弱选中色,随主题重设。
|
||
auto applyListSelection = [](QListWidget* lw) {
|
||
auto styleIt = [lw]() {
|
||
const bool dark = geopro::app::isDarkTheme();
|
||
const QString selBg = dark ? QStringLiteral("#33527A") : QStringLiteral("#C2D9F2");
|
||
const QString selFg = dark ? QStringLiteral("#E8F1FB") : QStringLiteral("#14385F");
|
||
lw->setStyleSheet(QStringLiteral("QListWidget::item:selected{ background:%1; color:%2; }"
|
||
"QListWidget::item:selected:!active{ background:%1;"
|
||
" color:%2; }")
|
||
.arg(selBg, selFg));
|
||
};
|
||
styleIt();
|
||
QObject::connect(eTheme, &ElaTheme::themeModeChanged, lw,
|
||
[styleIt](ElaThemeType::ThemeMode) { styleIt(); });
|
||
};
|
||
|
||
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
||
auto* datasetTabs = new QTabWidget();
|
||
auto* datasetList = new QListWidget();
|
||
applyListSelection(datasetList);
|
||
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
||
auto* fileList = new QListWidget();
|
||
applyListSelection(fileList);
|
||
datasetTabs->addTab(fileList, QStringLiteral("文件"));
|
||
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
|
||
auto* datasetBox = wrapWithHeader(
|
||
geopro::app::Glyph::Dataset, QStringLiteral("数据真实显示栏"), datasetTabs,
|
||
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
||
{geopro::app::Glyph::Upload, QStringLiteral("上传")}});
|
||
datasetDock->setWidget(datasetBox);
|
||
// 动态标题:选中测线后改为「数据集显示栏 · ERTx」(对齐原型)。
|
||
auto* datasetTitle = datasetBox->findChild<QLabel*>(QStringLiteral("panelTitle"));
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
|
||
|
||
// 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
|
||
auto* anomalyList = new QListWidget();
|
||
applyListSelection(anomalyList);
|
||
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
|
||
objAttrLabel->setWordWrap(true);
|
||
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||
objAttrLabel->setMargin(8);
|
||
|
||
auto anomalyPanel = geopro::app::buildTabbedPanel(
|
||
{{geopro::app::Glyph::Anomaly, QStringLiteral("异常列表"), anomalyList, true},
|
||
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrLabel, false}},
|
||
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
||
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
|
||
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
|
||
// colorize(C):异常计数用语义 warning“需注意”变体(区别于普通中性计数徽标),
|
||
// 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。
|
||
// 注:徽标的填充/显隐在 loadDataset 内(当前被 park),故此色与徽标本身同属休眠态,
|
||
// 接 dd 详情渲染那轮一并可见。
|
||
if (anomalyBadge) {
|
||
anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn"));
|
||
anomalyBadge->style()->unpolish(anomalyBadge);
|
||
anomalyBadge->style()->polish(anomalyBadge);
|
||
}
|
||
|
||
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
|
||
rightDock->setWidget(anomalyPanel.container);
|
||
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
|
||
|
||
// 右下 dock:属性(数据集属性,键值;对齐原型下半,独立面板)。
|
||
auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)"));
|
||
propLabel->setWordWrap(true);
|
||
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||
propLabel->setMargin(8);
|
||
auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
|
||
propDock->setWidget(
|
||
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel));
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
||
|
||
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
|
||
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
|
||
// 抽成 lambda:ADS restoreState() 恢复布局时会重建停靠区并重新显示标题栏,
|
||
// 故须在恢复布局之后再调用一次,确保任何已保存布局下标题栏都稳定隐藏。
|
||
const auto hideDockTitleBars = [&]() {
|
||
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
|
||
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
|
||
if (auto* area = d->dockAreaWidget())
|
||
if (auto* bar = area->titleBar()) bar->setVisible(false);
|
||
}
|
||
};
|
||
hideDockTitleBars();
|
||
|
||
// 中央编排已解耦到 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, kVerticalExaggeration);
|
||
};
|
||
|
||
// ── 数据详情共享状态 + 重建 ──────────────────────────────────────────
|
||
// 当前选中数据集 id(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
|
||
auto currentDsId = std::make_shared<QString>();
|
||
auto detailMode = std::make_shared<DetailMode>(DetailMode::Section18);
|
||
auto showAnomalies = std::make_shared<bool>(true); // 默认显示异常(对齐原型)
|
||
auto showElectrodes = std::make_shared<bool>(true); // 默认显示电极 ▼
|
||
auto showContour = std::make_shared<bool>(true); // 默认显示等值线
|
||
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
|
||
auto prevDsId = std::make_shared<QString>(); // 上次渲染的 DS id:判定“切换数据集”以触发揭示过渡
|
||
|
||
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
|
||
// 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。
|
||
// overdrive(A):仅“切换数据集”这一加载时刻播放相机补间 + actor 淡入揭示;模式/叠加层开关
|
||
// 属同一数据集内微调,直接落定不放动画(特殊时刻才特殊,避免每次交互都动的疲劳)。
|
||
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, detailWidget, currentDsId,
|
||
prevDsId, detailMode, showAnomalies, showElectrodes, showContour,
|
||
hiddenAnoms]() {
|
||
const bool dsChanged = (*currentDsId != *prevDsId);
|
||
const bool animate = dsChanged && !prevDsId->isEmpty() && !currentDsId->isEmpty();
|
||
|
||
// 过渡起点:清场景前先快照当前相机位姿。
|
||
auto fromCam = vtkSmartPointer<vtkCamera>::New();
|
||
fromCam->DeepCopy(detailRendererPtr->GetActiveCamera());
|
||
|
||
detailRendererPtr->RemoveAllViewProps();
|
||
{ // 背景随主题
|
||
double r, g, b;
|
||
geopro::app::vtkBackground(r, g, b);
|
||
detailRendererPtr->SetBackground(r, g, b);
|
||
}
|
||
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
|
||
*prevDsId = *currentDsId;
|
||
detailRenderWindowPtr->Render();
|
||
return;
|
||
}
|
||
std::vector<vtkSmartPointer<vtkActor>> added; // 本次加入的 actor,供淡入
|
||
const std::string id = currentDsId->toStdString();
|
||
if (*detailMode == DetailMode::Section18) {
|
||
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。
|
||
const auto g = repo.loadGrid(id);
|
||
const auto cs = repo.loadColorScale(id);
|
||
const auto actors = geopro::render::buildGridContour(g, cs);
|
||
if (actors.bands) {
|
||
actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||
detailRendererPtr->AddViewProp(actors.bands);
|
||
added.push_back(actors.bands);
|
||
}
|
||
if (actors.edges && *showContour) {
|
||
actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||
detailRendererPtr->AddViewProp(actors.edges);
|
||
added.push_back(actors.edges);
|
||
}
|
||
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
|
||
if (*showElectrodes) {
|
||
auto elec = geopro::render::buildElectrodes(g);
|
||
if (elec) {
|
||
elec->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||
detailRendererPtr->AddViewProp(elec);
|
||
added.push_back(elec);
|
||
}
|
||
}
|
||
} else {
|
||
// 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。
|
||
const auto s = repo.loadScatter(id);
|
||
const auto scs = repo.loadScatterColorScale(id);
|
||
auto a = geopro::render::buildScatter(s, scs, kScatterPointSize);
|
||
if (a) {
|
||
a->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||
detailRendererPtr->AddViewProp(a);
|
||
added.push_back(a);
|
||
}
|
||
}
|
||
// 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。
|
||
if (*showAnomalies) {
|
||
const auto anomalies = repo.loadAnomalies(id);
|
||
for (int i = 0; i < static_cast<int>(anomalies.size()); ++i) {
|
||
if (hiddenAnoms->count(i)) continue; // 列表中取消勾选→隐藏
|
||
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
|
||
act->SetScale(1.0, kVerticalExaggeration, 1.0);
|
||
detailRendererPtr->AddViewProp(act);
|
||
added.push_back(act);
|
||
}
|
||
}
|
||
}
|
||
geopro::render::applyTop2D(detailRendererPtr);
|
||
detailRendererPtr->ResetCamera();
|
||
*prevDsId = *currentDsId;
|
||
|
||
if (animate) {
|
||
// 目标位姿快照 → 相机回退到旧位姿 + actors 透明 → 补间到目标并淡入。
|
||
auto toCam = vtkSmartPointer<vtkCamera>::New();
|
||
toCam->DeepCopy(detailRendererPtr->GetActiveCamera());
|
||
for (const auto& a : added) a->GetProperty()->SetOpacity(0.0);
|
||
detailRendererPtr->GetActiveCamera()->DeepCopy(fromCam);
|
||
detailRendererPtr->ResetCameraClippingRange();
|
||
animateReveal(detailRendererPtr, detailRenderWindowPtr, fromCam, toCam, added, 450,
|
||
detailWidget);
|
||
} else {
|
||
detailRenderWindowPtr->Render();
|
||
}
|
||
};
|
||
|
||
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
|
||
auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms,
|
||
anomalyBadge](const QString& dsId, const QString& name) {
|
||
if (dsId.isEmpty()) return;
|
||
*currentDsId = dsId;
|
||
|
||
// 右上异常列表:按该数据集异常重填(默认全显);先清隐藏集再填,避免重建时阻塞信号回灌。
|
||
const auto anomalies = repo.loadAnomalies(dsId.toStdString());
|
||
hiddenAnoms->clear();
|
||
{
|
||
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽
|
||
geopro::app::populateAnomalyList(anomalyList, anomalies);
|
||
}
|
||
// 异常列表 Tab 数量徽标。
|
||
if (anomalyBadge) {
|
||
anomalyBadge->setText(QString::number(anomalies.size()));
|
||
anomalyBadge->setVisible(!anomalies.empty());
|
||
}
|
||
|
||
rebuildDetail();
|
||
|
||
// 右下属性(数据集级,与详情模式无关)。
|
||
const auto g = repo.loadGrid(dsId.toStdString());
|
||
propLabel->setText(
|
||
QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n"
|
||
"vmin / vmax: %4 / %5\n异常: %6 个")
|
||
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
||
.arg(anomalies.size()));
|
||
};
|
||
// 暂未触发:保留待下一轮真实 DS 详情渲染复用。
|
||
// TODO(overdrive-A 依赖):把下面数据集单击处理改调 loadDataset(dsId, name) 接通真实详情
|
||
// 渲染后,rebuildDetail 里已就绪的“相机补间 + actor 淡入”揭示动画会在切换数据集时自动激活
|
||
// (见 rebuildDetail 的 animate 分支与 animateReveal)。在此之前该动画为休眠态、不可见。
|
||
(void)loadDataset;
|
||
|
||
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
|
||
// 接 dd 那轮:把本处占位改为 loadDataset(id, name) 即接通详情渲染,并自动激活 overdrive-A 揭示动画。
|
||
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
|
||
[propLabel, detailRendererPtr, detailRenderWindowPtr, &nav](QListWidgetItem* item) {
|
||
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
|
||
nav.loadMoreData();
|
||
return;
|
||
}
|
||
const QString name =
|
||
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
|
||
detailRendererPtr->RemoveAllViewProps();
|
||
detailRenderWindowPtr->Render();
|
||
propLabel->setText(QStringLiteral(
|
||
"数据集: %1\n(该数据集的剖面/反演渲染将在下一阶段接入 dd 接口)").arg(name));
|
||
});
|
||
|
||
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
|
||
QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList,
|
||
[hiddenAnoms, rebuildDetail](QListWidgetItem* item) {
|
||
const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt();
|
||
if (item->checkState() == Qt::Checked)
|
||
hiddenAnoms->erase(idx);
|
||
else
|
||
hiddenAnoms->insert(idx);
|
||
rebuildDetail();
|
||
});
|
||
|
||
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
|
||
QObject::connect(actSection, &QAbstractButton::clicked, detailWidget,
|
||
[detailMode, rebuildDetail]() {
|
||
*detailMode = DetailMode::Section18;
|
||
rebuildDetail();
|
||
});
|
||
QObject::connect(actScatter, &QAbstractButton::clicked, detailWidget,
|
||
[detailMode, rebuildDetail]() {
|
||
*detailMode = DetailMode::Scatter17;
|
||
rebuildDetail();
|
||
});
|
||
|
||
// ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ──
|
||
QObject::connect(actShowAnomaly, &QAbstractButton::toggled, detailWidget,
|
||
[showAnomalies, rebuildDetail](bool on) {
|
||
*showAnomalies = on;
|
||
rebuildDetail();
|
||
});
|
||
QObject::connect(actShowElectrodes, &QAbstractButton::toggled, detailWidget,
|
||
[showElectrodes, rebuildDetail](bool on) {
|
||
*showElectrodes = on;
|
||
rebuildDetail();
|
||
});
|
||
QObject::connect(actShowContour, &QAbstractButton::toggled, detailWidget,
|
||
[showContour, rebuildDetail](bool on) {
|
||
*showContour = on;
|
||
rebuildDetail();
|
||
});
|
||
|
||
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。
|
||
auto showLayerPanel = [layerPanel, viewToolBar](bool show3D) {
|
||
if (show3D) {
|
||
layerPanel->move(14, viewToolBar->height() + 12);
|
||
layerPanel->adjustSize();
|
||
layerPanel->setVisible(true);
|
||
layerPanel->raise();
|
||
} else {
|
||
layerPanel->setVisible(false);
|
||
}
|
||
};
|
||
|
||
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ──
|
||
QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget,
|
||
[viewMode, rebuildCentral, showLayerPanel]() {
|
||
*viewMode = ViewMode::Map2D;
|
||
showLayerPanel(false);
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(act3D, &QAbstractButton::clicked, vtkWidget,
|
||
[viewMode, rebuildCentral, showLayerPanel]() {
|
||
*viewMode = ViewMode::View3D;
|
||
showLayerPanel(true);
|
||
rebuildCentral();
|
||
});
|
||
|
||
// ──「视图详情」图层勾选 → 更新图层显隐 → 重建中央 ──
|
||
QObject::connect(chkCurtain, &QCheckBox::toggled, vtkWidget,
|
||
[showCurtain, rebuildCentral](bool on) {
|
||
*showCurtain = on;
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(chkVoxel, &QCheckBox::toggled, vtkWidget,
|
||
[showVoxel, rebuildCentral](bool on) {
|
||
*showVoxel = on;
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget,
|
||
[showTerrain, rebuildCentral](bool on) {
|
||
*showTerrain = on;
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(chkSlice, &QCheckBox::toggled, vtkWidget,
|
||
[showSlice, rebuildCentral](bool on) {
|
||
*showSlice = on;
|
||
rebuildCentral();
|
||
});
|
||
|
||
// ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。
|
||
rebuildCentral();
|
||
|
||
// VTK 背景随主题切换:直接重跑 rebuildCentral/rebuildDetail(走完整渲染路径、末尾必 Render,
|
||
// 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。
|
||
QObject::connect(eTheme, &ElaTheme::themeModeChanged, &window,
|
||
[rebuildCentral, rebuildDetail]() {
|
||
rebuildCentral();
|
||
rebuildDetail();
|
||
});
|
||
|
||
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
|
||
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
|
||
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));
|
||
topBar = new geopro::app::TopBar(topChrome);
|
||
topLayout->addWidget(topBar);
|
||
window.setMenuWidget(topChrome);
|
||
}
|
||
|
||
// ── 控制器 ↔ UI 信号接线(导航壳)──────────────────────────────────────
|
||
// "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。
|
||
auto removeLoadMore = [](QListWidget* lw) {
|
||
if (lw->count() > 0 &&
|
||
lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool())
|
||
delete lw->takeItem(lw->count() - 1);
|
||
};
|
||
auto addLoadMore = [](QListWidget* lw, int total) {
|
||
const int loaded = lw->count();
|
||
if (loaded < total) {
|
||
auto* m = new QListWidgetItem(QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total), lw);
|
||
m->setData(geopro::app::kDsLoadMoreRole, true);
|
||
m->setTextAlignment(Qt::AlignCenter);
|
||
m->setForeground(QColor("#2D6CB5"));
|
||
}
|
||
return loaded;
|
||
};
|
||
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
|
||
&geopro::controller::WorkbenchNavController::switchWorkspace);
|
||
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
|
||
&geopro::controller::WorkbenchNavController::switchProject);
|
||
// 退出登录:清除记住的凭证(QtKeychain+QSettings) → 重启应用回到登录页。
|
||
QObject::connect(topBar, &geopro::app::TopBar::logoutRequested, &window, []() {
|
||
geopro::app::forgetSession();
|
||
QProcess::startDetached(QCoreApplication::applicationFilePath(),
|
||
QCoreApplication::arguments().mid(1));
|
||
qApp->quit();
|
||
});
|
||
QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window,
|
||
[&projectRepo, &nav, topBar, &window]() {
|
||
auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
QObject::connect(dlg, &geopro::app::ProjectListDialog::projectChosen, &nav,
|
||
[&nav, topBar](const QString& id, const QString& name) {
|
||
topBar->setProjectButtonText(name);
|
||
nav.switchProject(id);
|
||
});
|
||
dlg->exec();
|
||
});
|
||
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, int total) {
|
||
topBar->setProjects(list, cur, total > static_cast<int>(list.size()));
|
||
});
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
|
||
[objectTree, datasetList, fileList, datasetTitle, datasetTabs](
|
||
const QString& projectName,
|
||
const std::vector<geopro::data::StructNode>& nodes) {
|
||
objectTree->setStructure(projectName, nodes);
|
||
datasetList->clear();
|
||
fileList->clear();
|
||
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
||
datasetTabs->setTabText(0, QStringLiteral("数据"));
|
||
datasetTabs->setTabText(1, QStringLiteral("文件"));
|
||
});
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
|
||
[removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs](
|
||
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
|
||
bool append) {
|
||
removeLoadMore(datasetList);
|
||
geopro::app::populateDatasetList(datasetList, rows, append);
|
||
const int loaded = addLoadMore(datasetList, total);
|
||
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
|
||
datasetTabs->setTabText(
|
||
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
|
||
: QStringLiteral("数据"));
|
||
});
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
|
||
[removeLoadMore, addLoadMore, fileList, datasetTabs](
|
||
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
|
||
bool append) {
|
||
removeLoadMore(fileList);
|
||
geopro::app::populateFileList(fileList, rows, append);
|
||
const int loaded = addLoadMore(fileList, total);
|
||
datasetTabs->setTabText(
|
||
1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total)
|
||
: QStringLiteral("文件"));
|
||
});
|
||
QObject::connect(fileList, &QListWidget::itemClicked, fileList,
|
||
[&nav](QListWidgetItem* item) {
|
||
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles();
|
||
});
|
||
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));
|
||
// 状态栏错误反馈:临时染 danger 红,8s 后随消息超时还原全局中性色。
|
||
auto* sb = window.statusBar();
|
||
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
|
||
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
|
||
sb->showMessage(QStringLiteral("加载失败(%1):%2").arg(stage, msg), 8000);
|
||
QTimer::singleShot(8000, sb, [sb]() { sb->setStyleSheet(QString()); });
|
||
});
|
||
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")
|
||
.arg(QString::fromUtf8(kProjectCrs))
|
||
.arg(lat0, 0, 'f', 5)
|
||
.arg(lon0, 0, 'f', 5));
|
||
|
||
// ── dock 布局/窗口几何持久化 ──────────────────────────────────────────
|
||
// 恢复上次的停靠布局与窗口几何(须在全部 dock 创建后;ADS 按 dock 标题作键匹配)。
|
||
{
|
||
const QSettings settings;
|
||
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
|
||
if (!geo.isEmpty()) window.restoreGeometry(geo);
|
||
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray();
|
||
if (!dockState.isEmpty()) {
|
||
dockManager->restoreState(dockState);
|
||
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
|
||
hideDockTitleBars();
|
||
}
|
||
}
|
||
// 退出时保存当前布局与几何(aboutToQuit 早于 window 析构,dockManager/window 仍存活)。
|
||
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
|
||
QSettings settings;
|
||
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
|
||
settings.setValue(QStringLiteral("ui/dockState"), dockManager->saveState());
|
||
});
|
||
}
|
||
|
||
} // namespace
|
||
|
||
int main(int argc, char* argv[])
|
||
{
|
||
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
|
||
// 必须在 QApplication 构造前设置。
|
||
QApplication::setHighDpiScaleFactorRoundingPolicy(
|
||
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||
|
||
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
|
||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
||
QApplication app(argc, argv);
|
||
|
||
// 组织/应用名:QSettings 持久化(dock 布局、登录记忆等)按此定位存储位置。
|
||
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
||
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
||
|
||
// ElaApplication:Fluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 Ela 化,
|
||
// 两种壳都需要它(登录发生在选壳之前)。Ela 控件跟随 ElaTheme;标准控件仍由下面 QSS 接管。
|
||
eApp->init();
|
||
geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝
|
||
|
||
// 专业主题(Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。
|
||
// 跟随 ElaTheme 初始模式(可能随系统为暗),使登录窗与标准控件明暗一致(review M2)。
|
||
geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark);
|
||
|
||
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
|
||
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
|
||
if (qEnvironmentVariableIsEmpty("PROJ_DATA")) {
|
||
const QString appDir = QCoreApplication::applicationDirPath();
|
||
const QStringList candidates = {
|
||
appDir + "/proj",
|
||
appDir + "/../../vcpkg_installed/x64-windows/share/proj",
|
||
QStringLiteral(
|
||
"D:/Git/lanbingtech/geopro/build/release/vcpkg_installed/x64-windows/share/proj"),
|
||
};
|
||
for (const auto& c : candidates) {
|
||
if (QFile::exists(c + "/proj.db")) {
|
||
qputenv("PROJ_DATA", c.toUtf8());
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 网络层:共享会话 ApiClient + 登录编排 AuthService(RSA 公钥从 resources 读取)。
|
||
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
|
||
const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem");
|
||
geopro::net::AuthService auth(api, pem);
|
||
|
||
// 记住登录:若上次勾选「记住」且未超 30 天,凭证库里有有效 token → 免登录直接进。
|
||
QString token = geopro::app::recallValidToken(30);
|
||
if (token.isEmpty()) {
|
||
geopro::app::LoginWindow login(auth);
|
||
if (login.exec() != QDialog::Accepted) return 0;
|
||
token = login.token();
|
||
if (login.remember())
|
||
geopro::app::rememberSession(token); // 安全存 token + 时间戳
|
||
else
|
||
geopro::app::forgetSession(); // 未勾选:清除旧记忆
|
||
}
|
||
|
||
api.setToken(token); // 注入 token 供后续 API 使用
|
||
|
||
// 登录成功 → 构建并显示工作台。
|
||
geopro::data::LocalSampleRepository repo(
|
||
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
|
||
|
||
// 导航仓储 + 控制器(接口/逻辑层):用同一共享会话 ApiClient。
|
||
geopro::data::ApiProjectRepository projectRepo(api);
|
||
geopro::controller::WorkbenchNavController nav(projectRepo);
|
||
|
||
// ── 外壳:Fluent ElaWindow(唯一路径)。ElaWindow 用 addPageNode 包裹一个承载工作台的内层
|
||
// QMainWindow(buildWorkbench 依赖 QMainWindow 的 setCentralWidget/setMenuWidget/statusBar,
|
||
// ElaWindow 自身将其设为私有,故用内层 QMainWindow 承接,零改 buildWorkbench)。
|
||
const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)");
|
||
auto* ela = new ElaWindow;
|
||
ela->setWindowTitle(kTitle);
|
||
ela->resize(1280, 800);
|
||
ela->setMinimumSize(1024, 680);
|
||
ela->setIsNavigationBarEnable(false); // 纯中心内容,不显示左侧导航栏
|
||
ela->setAppBarHeight(38); // 默认 45 偏大,收紧标题栏更接近原生
|
||
auto* inner = new QMainWindow(ela); // 以 ela 为父,避免无父期调色板/DPI 抖动
|
||
buildWorkbench(*inner, repo, projectRepo, nav);
|
||
// 用 addPageNode 把工作台作为唯一页面放进中心页栈(填满到底边)。
|
||
// 注意:不能用 setCentralCustomWidget——它把控件插到页栈容器“之上”,空页栈仍占底部,
|
||
// 导致状态栏不贴底边(见 ElaCentralStackedWidget::setCustomWidget 的 insertWidget(0,...))。
|
||
ela->addPageNode(kTitle, inner);
|
||
|
||
// 主题桥:ElaTheme 明/暗切换 → 同步全局 QSS+调色板(覆盖所有标准控件与 ADS)。
|
||
QObject::connect(eTheme, &ElaTheme::themeModeChanged, ela, [&app](ElaThemeType::ThemeMode m) {
|
||
geopro::app::applyThemeMode(app, m == ElaThemeType::Dark);
|
||
});
|
||
geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // 初始对齐
|
||
// 主题切换快捷键 Ctrl+Shift+T(标题栏亦有 Ela 自带的明暗切换键)。
|
||
auto* themeSc = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+T")), ela);
|
||
QObject::connect(themeSc, &QShortcut::activated, ela, [] {
|
||
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark
|
||
: ElaThemeType::Light);
|
||
});
|
||
ela->show();
|
||
|
||
nav.start(); // 进入工作台后拉真实 空间/项目/结构
|
||
|
||
return app.exec();
|
||
}
|