Compare commits
10 Commits
127e9a0b21
...
72761fc05f
| Author | SHA1 | Date |
|---|---|---|
|
|
72761fc05f | |
|
|
e27a93073a | |
|
|
a0fcc4cc62 | |
|
|
6c60f5a83d | |
|
|
65283492fb | |
|
|
f57291a127 | |
|
|
7007619bf2 | |
|
|
8466fe3a5a | |
|
|
a2efef8ada | |
|
|
50c4de4019 |
|
|
@ -67,9 +67,10 @@ setx VCPKG_ROOT "C:\dev\vcpkg" # 永久(新开终端生效)
|
||||||
|
|
||||||
**用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用):
|
**用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用):
|
||||||
|
|
||||||
1. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。
|
1. `.\qt-online-installer-windows-x64-4.11.0.exe --mirror https://ftp.jaist.ac.jp/pub/qtproject`
|
||||||
2. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。
|
2. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。
|
||||||
3. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。
|
3. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。
|
||||||
|
4. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。
|
||||||
|
|
||||||
CMake 经 `CMAKE_PREFIX_PATH=D:/Qt/6.11.1/msvc2022_64` 找到它(见 §6 预设)。**全链路只此一份 Qt**。
|
CMake 经 `CMAKE_PREFIX_PATH=D:/Qt/6.11.1/msvc2022_64` 找到它(见 §6 预设)。**全链路只此一份 Qt**。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,15 @@
|
||||||
桌面 app `geopro_desktop`:
|
桌面 app `geopro_desktop`:
|
||||||
- **真实登录**:LoginWindow(用户名/密码/图形验证码/记住)→ `verifyCodeCheck` → RSA 加密密码 → `login2` → token → 进工作台。**真机登录成功**;输入框样式已修(三态白底深字)。
|
- **真实登录**:LoginWindow(用户名/密码/图形验证码/记住)→ `verifyCodeCheck` → RSA 加密密码 → `login2` → token → 进工作台。**真机登录成功**;输入框样式已修(三态白底深字)。
|
||||||
- **工作台 ADS 三区 + 数据详情**:
|
- **工作台 ADS 三区 + 数据详情**:
|
||||||
- **左 对象树**:GS→TM→DS,复选框勾选 → 控制中央显示哪些测线。
|
- **左上 对象显示栏**:GS→TM(测线),复选框勾选测线 → 控制中央显示哪些测线的 dd_section。
|
||||||
|
- **左下 数据真实显示栏**(对齐原型):单击测线 → 列其采集批次(数据集,tab 数据/文件);单击采集批次 → 数据详情+异常列表+属性。启动自动选首测线+首数据集。
|
||||||
- **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换):
|
- **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换):
|
||||||
- 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。
|
- 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。
|
||||||
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」(对齐原型,无体素按钮)。
|
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」。**3D 左上「视图详情」浮层**(对齐原型,仅 3D 显示)五图层勾选:**帘面 / 体素 / 切片 / 地形**(体素=`buildVoxelFromScatters` 散点经 EPSG:4547 配准 IDW;切片=`vtkImagePlaneWidget` 在体素 image 上交互拖切面=dd_slice;地形=`TerrainActor` GDAL 读 DEM 高程面 + 影像 EPSG:3857 重投影贴图;后三者需 PROJ,不可用则禁用)。
|
||||||
- **下方 数据详情**:工具条「原数据/网格数据」切换 +「显示异常」开关(对齐原型命名)。单击数据集 → 网格数据=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17,x=距离/y=深度取负,用散点自带色阶);显示异常=`AnomalyActor` 在上图叠加异常 dashed 折线(同纵向夸张对齐)。
|
- **下方 数据详情**:工具条「原数据/网格数据」切换 +「显示异常/显示电极/显示等值线」开关(对齐原型)。单击数据集 → 网格数据=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17);显示异常=`AnomalyActor` dashed 折线叠加;显示电极=`ElectrodeActor` 顶边 ▼ 标记;显示等值线=#18 黑色等值线显隐(同纵向夸张对齐)。
|
||||||
- **右上 异常列表**(对齐原型):单击数据集→列该数据集异常(颜色块+名称(类型)+派生「位置/深/尺寸」),勾选框显隐,与数据详情异常叠加联动(取消勾选→该异常虚线隐藏)。
|
- **右上 异常列表**(对齐原型):单击数据集→列该数据集异常(颜色块+名称(类型)+派生「位置/深/尺寸」),勾选框显隐,与数据详情异常叠加联动(取消勾选→该异常虚线隐藏)。
|
||||||
- **右下 属性**:名称/网格 nx×ny/vmin·vmax/异常数。
|
- **右下 属性**:名称/网格 nx×ny/vmin·vmax/异常数。
|
||||||
- 单元测试累计 **36 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly/voxel_top/voxel_3d.png` 均核对正确(scatter 吻合 ref_17、异常吻合 ref_18、体素 footprint 吻合 ref voxel_hslice 的两臂支撑)。
|
- 单元测试累计 **40 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1 + Electrode 2 + Terrain 2、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly(含电极▼)/voxel_top/voxel_3d/terrain_3d(DEM+影像贴图).png` 均核对正确。
|
||||||
|
|
||||||
## 2. 各 Phase 完成度
|
## 2. 各 Phase 完成度
|
||||||
|
|
||||||
|
|
@ -30,7 +31,7 @@
|
||||||
| P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ |
|
| P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ |
|
||||||
| P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ |
|
| P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ |
|
||||||
| P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) |
|
| P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) |
|
||||||
| P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面)+ 数据详情(#18/#17/异常) | 🔶 **三视图 + 散点#17 + 异常叠加 已对; dd_voxel 引擎已验证(UI 未接,待 3D 图层控制)**;**DEM地形(需加gdal) / 底图瓦片 / dd_slice / 布局对齐原型(左下数据列表/右上异常列表/电极/3D图层浮层) 未做** |
|
| P4 | 渲染:render 层 + 二维地图(线)+ 三维(帘面/体素/切片/地形)+ 数据详情(#18/#17/异常/电极) | ✅ **核心全达成**:三视图 + #17 + 异常 + dd_voxel + dd_slice + DEM地形影像 + 原型六面板。**剩**:底图瓦片(M1.5)、数值标签、Credential 免登录、Z 基准统一、dock 透视持久化 |
|
||||||
|
|
||||||
## 3. 构建约定(**机器本地**)
|
## 3. 构建约定(**机器本地**)
|
||||||
|
|
||||||
|
|
@ -39,7 +40,8 @@
|
||||||
- **构建/测试经** `& cmd /c "D:\Git\lanbingtech\geopro\external\dev.bat <cmd>"`(**PowerShell 调**,Bash 下参数透传坏)。C: 极小→TEMP/构建全在 D:。
|
- **构建/测试经** `& cmd /c "D:\Git\lanbingtech\geopro\external\dev.bat <cmd>"`(**PowerShell 调**,Bash 下参数透传坏)。C: 极小→TEMP/构建全在 D:。
|
||||||
- **app 构建前先** `taskkill /IM geopro_desktop.exe /F`(运行中 LNK1104 锁 exe)。
|
- **app 构建前先** `taskkill /IM geopro_desktop.exe /F`(运行中 LNK1104 锁 exe)。
|
||||||
- 部署:`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe --release <exe>`(找不到 ads dll 的警告无害);VTK/vcpkg dll 由 POST_BUILD `TARGET_RUNTIME_DLLS` 拷。
|
- 部署:`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe --release <exe>`(找不到 ads dll 的警告无害);VTK/vcpkg dll 由 POST_BUILD `TARGET_RUNTIME_DLLS` 拷。
|
||||||
- **离屏渲染验证**:`render_verify` 需 PATH 加 `external\vtk-install\bin` + `build\release\vcpkg_installed\x64-windows\bin` 再运行。
|
- **离屏渲染验证**:`render_verify` 需 PATH 加 `external\vtk-install\bin` + `build\release\vcpkg_installed\x64-windows\bin`;且体素/地形需 `PROJ_DATA=...\vcpkg_installed\x64-windows\share\proj`、`GDAL_DATA=...\share\gdal` 再运行。
|
||||||
|
- **GDAL**:已加 vcpkg `gdal`(首次编译久,连带 hdf5/netcdf/geos 等;已缓存)。app/测试运行时需 PROJ_DATA(app main() 已自动按候选路径设;部署须随包附带 proj/gdal 数据)。
|
||||||
- **改源码用 Write 工具,勿用 PowerShell `Set-Content -Encoding UTF8`**(会把中文注释弄乱、断构建)。
|
- **改源码用 Write 工具,勿用 PowerShell `Set-Content -Encoding UTF8`**(会把中文注释弄乱、断构建)。
|
||||||
|
|
||||||
## 4. 关键决策与已核实事实
|
## 4. 关键决策与已核实事实
|
||||||
|
|
@ -67,8 +69,10 @@
|
||||||
|
|
||||||
1. ~~**散点 #17**:`ScatterActor`(剖面原数据 2597 点彩色散点),数据详情"原数据"视图~~ ✅ **已完成**(2026-06-08,离屏 PNG 核对吻合 Python 真值,接入数据详情「反演剖面/原数据」切换;app 待人工登录肉眼复核交互)。
|
1. ~~**散点 #17**:`ScatterActor`(剖面原数据 2597 点彩色散点),数据详情"原数据"视图~~ ✅ **已完成**(2026-06-08,离屏 PNG 核对吻合 Python 真值,接入数据详情「反演剖面/原数据」切换;app 待人工登录肉眼复核交互)。
|
||||||
2. ~~**异常叠加**:`AnomalyActor`(markType 点/线/面)~~ ✅ **已完成**(2026-06-08,叠加在数据详情 #18/#17 上,「显示异常」开关默认开;离屏 `verify_section_anomaly.png` 折线位置吻合 ref_18;样本 3 异常均 markType=2 dashed;app 待人工登录复核)。**注**:dashed 点画在 VTK OpenGL2 下偏弱(几乎实线),几何/颜色/位置正确,纯观感项可后续调。
|
2. ~~**异常叠加**:`AnomalyActor`(markType 点/线/面)~~ ✅ **已完成**(2026-06-08,叠加在数据详情 #18/#17 上,「显示异常」开关默认开;离屏 `verify_section_anomaly.png` 折线位置吻合 ref_18;样本 3 异常均 markType=2 dashed;app 待人工登录复核)。**注**:dashed 点画在 VTK OpenGL2 下偏弱(几乎实线),几何/颜色/位置正确,纯观感项可后续调。
|
||||||
3. **DEM/影像地形**:加 vcpkg `gdal`;GDAL 读 dem.tif/image.tif;**影像 EPSG:3857 必须 PROJ 重投影到世界系**;`vtkWarpScalar` 地形面 + 纹理。
|
3. ~~**DEM/影像地形**~~ ✅ **已完成**(2026-06-08)。`render::buildTerrain`(GDAL 读 dem.tif 高程 + image.tif 影像;DEM CRS→4326→GeoLocalFrame 配准成 warp 面;影像 EPSG:3857→像素纹理坐标贴图)。离屏 `verify_terrain_3d.png` 卫星影像正确贴微起伏面、方向正立、配准对位。**注**:影像须用 **GDAL 读像素**(vtkTIFFReader 对此压缩 TIFF 报错);+2 单测;接入 app 3D「地形」图层。dem.tif 低分辨率→起伏细微。Z 基准与帘面/体素夸张未统一(spec M-3 待办)。
|
||||||
4. **dd_voxel 回归**:🔶 **引擎已完成并验证**(2026-06-08,CRS 已定 EPSG:4547)。`render::buildVoxelFromScatters`:散点 projX/Y→4547→4326→GeoLocalFrame 配准 + IDW(maxDist 裁剪)→ `buildVoxel`;离屏 `verify_voxel_top.png` 两臂支撑吻合 ref voxel_hslice、`verify_voxel_3d.png` profile1 片贴合帘面;+1 单测(VoxelRegister,需 PROJ_DATA)。**UI 未接入**:曾加"体素"工具条开关,但与二维/三维平级令人困惑且不在原型,**已移除**;待做 **3D 图层控制(对齐原型「视图详情」浮层)** 再正经接入(届时 main() 需设 PROJ_DATA + 部署随包附带 proj 数据)。**注**:仅 2 交叉线→薄十字片(15.9% 充填,半透明偏淡),可信满体需≥3线(设计 §10/§14)。**dd_slice 交互切片未做**(buildVoxel 已暴露 image 供 reslice widget)。
|
- **dd_slice 交互切片** ✅ 已完成(同日):3D「切片」图层=`vtkImagePlaneWidget` 在体素 image 拖切面(spec M1-b);**左键拖动=移动切面**(默认左键取值光标已改),中键=取值;纵向夸张已烤进体素 image(zDisplayScale),切片/体绘制/帘面同坐标系(切片穿过体素)。
|
||||||
|
- **性能排查结论(2026-06-08,实测)**:体绘制走 **GPU(RTX3060)**,拖动每帧~7–15ms(非 CPU 回退,前判误);拖动流畅。**唯一卡点=首次开体素/切片的 GPU 着色器编译+体上传(一次性 ~1.2–1.7s)**,属固有成本,M1 可接受(如需消除可后续 pre-warm)。体绘制设 Auto/InteractiveAdjustSampleDistances=0(全程统一全质量,GPU 够快)。教训:勿臆测性能根因(CPU回退/磁盘flush 均被实测否定),用帧间隙时间戳实测定位。
|
||||||
|
4. **dd_voxel 回归**:✅ **已完成**(2026-06-08,CRS 已定 EPSG:4547)。`render::buildVoxelFromScatters`:散点 projX/Y→4547→4326→GeoLocalFrame 配准 + IDW(maxDist 裁剪)→ `buildVoxel`;离屏 `verify_voxel_top.png` 两臂支撑吻合 ref voxel_hslice、`verify_voxel_3d.png` profile1 片贴合帘面;+1 单测(VoxelRegister,需 PROJ_DATA)。**UI 已接入**(增量3):3D「视图详情」浮层「体素」图层勾选驱动;main() 自动设 PROJ_DATA(部署须随包附带 proj 数据);PROJ 不可用则该层禁用。**注**:仅 2 交叉线→薄十字片(15.9% 充填,半透明偏淡),可信满体需≥3线(设计 §10/§14)。**dd_slice 交互切片未做**(buildVoxel 已暴露 image 供 reslice widget)。
|
||||||
5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。
|
5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。
|
||||||
6. **Credential(QtKeychain)**:记住一个月免登录持久化(P3 Task2 未做)。
|
6. **Credential(QtKeychain)**:记住一个月免登录持久化(P3 Task2 未做)。
|
||||||
7. 多测线:当前样本仅 1 条 dd_section(grid1);多条共存机制已就绪,加数据即叠加。
|
7. 多测线:当前样本仅 1 条 dd_section(grid1);多条共存机制已就绪,加数据即叠加。
|
||||||
|
|
@ -76,9 +80,10 @@
|
||||||
9. render 仍部分内联在 main.cpp;可逐步抽到 view/controller。
|
9. render 仍部分内联在 main.cpp;可逐步抽到 view/controller。
|
||||||
10. **布局对齐原型**(权威参考 `http://prototype.geomative.cn/`;截图存 `.playwright-mcp/`)。**计划见 `plans/2026-06-08-m1-prototype-layout.md`(六面板 + view/ 抽取,增量序列)**。进度:
|
10. **布局对齐原型**(权威参考 `http://prototype.geomative.cn/`;截图存 `.playwright-mcp/`)。**计划见 `plans/2026-06-08-m1-prototype-layout.md`(六面板 + view/ 抽取,增量序列)**。进度:
|
||||||
- ✅ **增量1 右上「异常列表」**(2026-06-08,`panels/AnomalyListPanel`,与数据详情显隐联动;待人工复核)。
|
- ✅ **增量1 右上「异常列表」**(2026-06-08,`panels/AnomalyListPanel`,与数据详情显隐联动;待人工复核)。
|
||||||
- ⬜ **增量2 左下「数据列表」**+ 对象树到 TM 层(DS 移出树入数据列表)。
|
- ✅ **增量2 左下「数据列表」+ 对象树到 TM 层**(2026-06-08,`panels/DatasetListPanel`;树 GS→TM 复选驱动中央, DS 移出树入数据列表 tab 数据/文件, DS 单击→详情+异常+属性, 启动自动选首测线/首数据集;待人工复核)。
|
||||||
- ⬜ **增量3 3D「视图详情」图层浮层**(体素的正确归宿;体素引擎已就绪,UI 待此接入)。
|
- ✅ **增量3 3D「视图详情」图层浮层**(2026-06-08,QFrame 浮于 QVTK 左上,仅 3D 显示;帘面/体素图层勾选;体素经此正经接入=之前移除的工具条开关的正确归宿;main() 设 PROJ_DATA;待人工复核浮层渲染+体素显隐)。
|
||||||
- ⬜ **增量4 数据详情富工具条 + 电极标记 + 数值标签**;底图影像=DEM/底图任务。
|
- 🔶 **增量4 电极标记 + 工具条**(2026-06-08,`ElectrodeActor` 顶边 ▼ PNG 核对吻合; +「显示电极/显示等值线」开关;待人工复核)。**剩**:数值标签、色阶配置/滤波处理(M1.5/进阶)。
|
||||||
|
- 渲染积木累计含 `ElectrodeActor`(顶边 ▼)、`TerrainActor`(DEM 高程面 + 影像纹理,GDAL)。底图瓦片=M1.5。
|
||||||
- 架构:新面板抽到 `src/app/panels/`(暂随 app 编译,如 login/),控制 main.cpp 体量;后续可升 `src/view/` 库。
|
- 架构:新面板抽到 `src/app/panels/`(暂随 app 编译,如 login/),控制 main.cpp 体量;后续可升 `src/view/` 库。
|
||||||
|
|
||||||
## 7. 渲染验证手段(务必用)
|
## 7. 渲染验证手段(务必用)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,11 @@
|
||||||
- 放右上 dock(与"属性"分上下或 tab)。**人工复核**:单击数据集→右上列出 3 异常;眼睛切换隐藏对应虚线。
|
- 放右上 dock(与"属性"分上下或 tab)。**人工复核**:单击数据集→右上列出 3 异常;眼睛切换隐藏对应虚线。
|
||||||
- 提交 `feat(view): 右上异常列表面板 + 与数据详情异常显隐联动`。
|
- 提交 `feat(view): 右上异常列表面板 + 与数据详情异常显隐联动`。
|
||||||
|
|
||||||
### 增量 2:左下「数据列表」+ 树结构调整(树到 TM,DS 入数据列表)
|
### 增量 2:左下「数据列表」+ 树结构调整 ✅ 已完成(2026-06-08)
|
||||||
|
> `panels/DatasetListPanel`(populateDatasetList)。树 `populateTree` 改 GS→TM(测线复选,UserRole+2=tmId);
|
||||||
|
> `findTm` 按 tmId 查 TM。中央 rebuildCentral 改遍历勾选 TM→渲染其 dd_section。左下 dock = QTabWidget
|
||||||
|
> (数据 QListWidget / 文件占位)。树单击 TM→填数据列表;数据列表单击 DS→`loadDataset`(详情+异常+属性,
|
||||||
|
> 抽成共享 lambda)。启动自动选首含 dd_section 的测线+首数据集。app 构建净;**待人工复核**。**原计划存档**:
|
||||||
- repo.loadStructure 改为树到 **TM** 层(GS→TM),DS 不再进树;新增 `listDatasets(tmId)` 或复用结构。
|
- repo.loadStructure 改为树到 **TM** 层(GS→TM),DS 不再进树;新增 `listDatasets(tmId)` 或复用结构。
|
||||||
(当前样本仅 1 DS grid1 挂 ERT1;多 TM/DS 为 mock 结构,先支持单条,预留多条。)
|
(当前样本仅 1 DS grid1 挂 ERT1;多 TM/DS 为 mock 结构,先支持单条,预留多条。)
|
||||||
- `src/view/panels/DatasetListPanel`:tab 数据/文件;列选中 TM 的采集批次;单击 → 数据详情 + 属性。
|
- `src/view/panels/DatasetListPanel`:tab 数据/文件;列选中 TM 的采集批次;单击 → 数据详情 + 属性。
|
||||||
|
|
@ -66,19 +70,32 @@
|
||||||
- **人工复核**:点 ERT1 → 左下列出采集批次 → 单击 → 数据详情出图。
|
- **人工复核**:点 ERT1 → 左下列出采集批次 → 单击 → 数据详情出图。
|
||||||
- 提交 `feat(view): 左下数据列表 + 对象树到测线层(DS 移出树)`。
|
- 提交 `feat(view): 左下数据列表 + 对象树到测线层(DS 移出树)`。
|
||||||
|
|
||||||
### 增量 3:3D「视图详情」图层浮层(体素的正确归宿)
|
### 增量 3:3D「视图详情」图层浮层 ✅ 已完成(2026-06-08)
|
||||||
|
> main.cpp 内联 QFrame 浮层(child of centerWidget,move 到 QVTK 左上=工具条下方,raise;仅 3D setVisible)。
|
||||||
|
> 含 帘面/体素 QCheckBox。rebuildCentral:帘面层 gate buildCurtain;体素层→buildVoxelFromScatters 加体绘制
|
||||||
|
> (同纵向夸张)。showCurtain/showVoxel/crs 共享态。main() 自动设 PROJ_DATA;crs 失败→体素勾选禁用。
|
||||||
|
> app 构建净;**待人工复核**(浮层是否正确浮于 QVTK 之上、勾体素是否出十字片)。**原计划存档**:
|
||||||
- 中央三维视图左上浮层(QWidget overlay on QVTK,或工具区):勾选显示 测线/帘面/**体素**/(地形)。
|
- 中央三维视图左上浮层(QWidget overlay on QVTK,或工具区):勾选显示 测线/帘面/**体素**/(地形)。
|
||||||
- 勾"体素" → 调 `buildVoxelFromScatters`(已验证)加入 3D 场景;勾选驱动 rebuildCentral 的图层集。
|
- 勾"体素" → 调 `buildVoxelFromScatters`(已验证)加入 3D 场景;勾选驱动 rebuildCentral 的图层集。
|
||||||
- main() 设 PROJ_DATA(体素配准需 proj.db;按候选路径,部署随包附带)。
|
- main() 设 PROJ_DATA(体素配准需 proj.db;按候选路径,部署随包附带)。
|
||||||
- **人工复核**:三维视图勾"体素"→ 出十字片体素,与帘面同系;取消则移除。
|
- **人工复核**:三维视图勾"体素"→ 出十字片体素,与帘面同系;取消则移除。
|
||||||
- 提交 `feat(view): 3D 视图详情图层浮层(测线/帘面/体素) + 体素正经接入`。
|
- 提交 `feat(view): 3D 视图详情图层浮层(测线/帘面/体素) + 体素正经接入`。
|
||||||
|
|
||||||
### 增量 4:数据详情富工具条 + 电极 + 数值标签(对齐原型中下)
|
### 增量 4:数据详情富工具条 + 电极 🔶 部分完成(2026-06-08)
|
||||||
|
> ✅ `actors/ElectrodeActor`(buildElectrodes:顶边各 x 列朝下三角 ▼,离屏 PNG 核对吻合) + 数据详情
|
||||||
|
> 「显示电极/显示等值线」开关(显示等值线 gate #18 edges actor)。+2 单测,全 38 测试绿。
|
||||||
|
> ⬜ 剩:数值标签(vtkLabeledDataMapper)、色阶配置/滤波处理(进阶/M1.5)。**原计划存档**:
|
||||||
- 工具条补 色阶配置/显示电极/显示等值线 等(按需,部分 M1 可占位)。
|
- 工具条补 色阶配置/显示电极/显示等值线 等(按需,部分 M1 可占位)。
|
||||||
- 剖面顶部电极▼标记(grid.lat/lon 或 x 轴电极位);可选数值标签。
|
- 剖面顶部电极▼标记(grid.lat/lon 或 x 轴电极位);可选数值标签。
|
||||||
- 提交 `feat(view): 数据详情电极标记 + 工具条对齐原型`。
|
- 提交 `feat(view): 数据详情电极标记 + 工具条对齐原型`。
|
||||||
|
|
||||||
### (增量 5,可选)右下属性面板规范化 + 整体 dock 透视持久化
|
### 增量 5:DEM 地形 + dd_slice ✅ 已完成(2026-06-08, backlog 项随原型 3D 图层一并落地)
|
||||||
|
> `actors/TerrainActor`(GDAL 读 DEM/影像 → 重投影到 GeoLocalFrame → warp 面 + GDAL 读影像像素作纹理;
|
||||||
|
> 影像 EPSG:3857 重投影对位)。离屏 `verify_terrain_3d.png` 核对吻合(卫星影像正立贴面)。+2 单测。
|
||||||
|
> dd_slice = `vtkImagePlaneWidget` 在体素 image 拖切面。两者接入 3D「视图详情」浮层(地形/切片图层)。
|
||||||
|
> 全 40 测试绿。vcpkg 加 gdal。**注**:影像须 GDAL 读(非 vtkTIFFReader);Z 基准统一(M-3)待办。
|
||||||
|
|
||||||
|
### (后续,可选)数值标签 / 色阶配置 / Credential 免登录 / dock 透视持久化 / 底图瓦片(M1.5)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
# Geopro 3.0 桌面客户端 — M1 架构设计
|
||||||
|
|
||||||
|
**日期**:2026-06-07
|
||||||
|
**版本**:v2(已按双专家评审 + 数据核验修订;修订点见 §16)
|
||||||
|
**状态**:待用户复核 v2
|
||||||
|
**范围**:M1 里程碑 = 完整工作台外壳 + 登录功能 + 三维视图(基础渲染 / dd_voxel 体绘制与切片 / DEM 地形)
|
||||||
|
**上位文档**:`docs/Geopro3.0_技术选型与架构规约.md`(技术基线,本文遵从其全部约束)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标与范围
|
||||||
|
|
||||||
|
### 1.1 M1 交付目标
|
||||||
|
|
||||||
|
复刻 Geopro 3.0 最核心的「项目分析视图」桌面版,并把登录做实:
|
||||||
|
|
||||||
|
1. **登录功能完全可用**:真实连接生产后端(`pop-api`),走验证码 + RSA 加密密码流程,token 安全存储。登录页样式参考现有 web 系统。
|
||||||
|
2. **完整工作台外壳**:ADS 三区停靠布局,还原原型(左:对象树 + 数据集列表;中:2D/3D 视图 + 数据详情;右:异常列表/对象属性 + 属性)。
|
||||||
|
3. **三维视图**(M1 核心难点):
|
||||||
|
- ① 基础渲染:剖面散点、网格等值面/等值线/标注、异常圈定(直接渲染)
|
||||||
|
- ② dd_voxel 体绘制 + 鼠标交互切片(dd_slice)——C++ 进程内三维插值;**追求可信体**(非演示性),故对输入数据有要求(见 §10、§13 分阶段)
|
||||||
|
- ④ DEM 地形起伏 + 影像贴图
|
||||||
|
- 二维俯视相机预设(验证「单一场景」架构)
|
||||||
|
4. **业务数据来源**:登录联网;工作台业务数据 M1 用本地样本文件,经 Repository 抽象注入,未来无缝切 API。
|
||||||
|
|
||||||
|
### 1.2 不在 M1 范围
|
||||||
|
|
||||||
|
- ③ 雷达单/多通道渲染、⑤ 在线底图瓦片 → M1.5
|
||||||
|
- 反演/数据处理算法本体(M1 只做「展示期插值」,不做反演)
|
||||||
|
- 项目管理、设备连接、在线监测、报告、平台后台、Web 端
|
||||||
|
- 完整算法插件架构(进程隔离 + manifest)→ 规约 D-3 推迟;M1 仅以 `IInterpolator` 接口预留
|
||||||
|
- 在线更新三通道(规约 §8)
|
||||||
|
- macOS 构建(M1 先 Windows / MSVC 2022,架构保持跨平台可移植)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 关键决策记录
|
||||||
|
|
||||||
|
| 编号 | 决策 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| K-1 | 2D/3D 视图架构 | **单一 VTK 三维场景 + 相机预设切换**;底图做可插拔 GroundLayer(规约 §5.3、D-5)。被现有 web「Cesium 单 3D 引擎」实践印证。 |
|
||||||
|
| K-2 | 启动节奏 | 先出设计文档 → **spike 预研门槛** → 再写完整实现计划 |
|
||||||
|
| K-3 | M1 外壳范围 | 完整工作台(A 方案) |
|
||||||
|
| K-4 | 业务数据来源 | 登录走真实 API;业务数据本地样本 + Repository 抽象(B 方案) |
|
||||||
|
| K-5 | M1 三维内容 | ① 基础 + ② dd_voxel(可信体,图分阶段)+ ④ DEM;③ 雷达、⑤ 底图瓦片留 M1.5 |
|
||||||
|
| K-6 | 三维插值实现 | C++ 进程内(IDW 起步),`IInterpolator` 接口隔离、**返回 core 中立类型**;推迟完整插件架构(D-1/D-3) |
|
||||||
|
| K-7 | 坐标系 | **每数据源各记源 CRS + 各自 LocalFrame** → 统一 rebase 到唯一「项目世界系(局部米,含 Z 基准)」;GIS/经纬/底图用 PROJ 实时换算(见 §5) |
|
||||||
|
| K-8 | 构建/部署 | **方案②-修订**(经双专家评审+实机勘验改定):单一 Qt = **官方 MSVC 预编译 Qt**(`D:\Qt\6.11.1\msvc2022_64`);**VTK/ADS/QtKeychain 对接该官方 Qt**(VTK 源码编到 install 前缀、ADS/QtKeychain 走 FetchContent),**绝不走 vcpkg**(否则 vcpkg 再拉一份 Qt = 双份);仅非 Qt 依赖(GDAL/PROJ/OpenSSL/Eigen/...)走 vcpkg。**关键事实**:用户原装的 `D:\Qt\6.11.1` 是 **MinGW 版**,MSVC 下不可链,须补装 MSVC kit;VTK 无 MSVC 预编译、三方案均须源码编;VS18=MSVC 14.51 链官方 Qt(v143)属"新链旧"ABI 安全。 |
|
||||||
|
| K-9 | 视图 widget | 评估 **`QVTKOpenGLStereoWidget`(QOpenGLWidget 系)** 优先于 native,缓解 ADS reparent 上下文丢失(spike 验证) |
|
||||||
|
| K-10 | dd_voxel 可信度 | 维持可信体目标;可信度取决于输入数据充分性(≥3 非共线剖面或 3D 网格),列为数据依赖(见 §10、§14) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 分层架构与目录结构
|
||||||
|
|
||||||
|
遵循规约 §10.3 清晰分层(core / data / view / controller / app),细分 net、render。
|
||||||
|
|
||||||
|
```
|
||||||
|
geopro/
|
||||||
|
├─ CMakeLists.txt / CMakePresets.json / vcpkg.json
|
||||||
|
├─ .clang-format / .clangd # AI 编码上下文基础设施(规约 §10.1)
|
||||||
|
├─ cmake/ # Find 模块、打包、dll 部署
|
||||||
|
├─ src/
|
||||||
|
│ ├─ core/ # 纯业务,零 Qt / 零 VTK 依赖(可独立单测)
|
||||||
|
│ │ ├─ model/ # Project, GsObject, TmObject, DsObject, Anomaly, ColorScale, Grid, ScatterField, ScalarVolume
|
||||||
|
│ │ ├─ geo/ # LocalFrame(原点+Z基准+轴向)、CrsTransform(PROJ 封装,多 CRS)
|
||||||
|
│ │ └─ algo/ # IInterpolator 接口 + IdwInterpolator(返回 core 的 ScalarVolume,绝不含 VTK)
|
||||||
|
│ ├─ data/ # 数据访问层(异步契约)
|
||||||
|
│ │ ├─ repo/ # IProjectRepository, IDatasetRepository(QFuture/回调 + 取消 + 分页)
|
||||||
|
│ │ ├─ local/ # LocalSampleRepository(QtConcurrent 线程池跑解析)+ 各格式解析器
|
||||||
|
│ │ ├─ api/ # ApiRepository(M1 骨架,签名对齐)
|
||||||
|
│ │ └─ dto/ # 后端 JSON DTO + → model 映射
|
||||||
|
│ ├─ net/ # ApiClient(QtNetwork)/ AuthService(验证码+RSA+login2)/ Credential(QtKeychain)
|
||||||
|
│ ├─ render/ # VTK 渲染层(独占 vtkRenderWindow,统一管理所有 actor)
|
||||||
|
│ │ ├─ Scene # 场景图、世界坐标空间、可见性;持有 RenderWindow
|
||||||
|
│ │ ├─ actors/ # ScatterActor, GridContourActor, VoxelVolumeActor, AnomalyActor, TerrainActor
|
||||||
|
│ │ ├─ color/ # ColorLutBuilder(colorBar → vtkLookupTable 离散阶梯), ScalarBar
|
||||||
|
│ │ ├─ camera/ # CameraPreset(Top2D / Free3D)
|
||||||
|
│ │ ├─ interact/ # InteractionManager + InteractionTool(MeasureTool/SliceTool/PickSelectTool)
|
||||||
|
│ │ └─ ground/ # IGroundLayer + DemImageGroundLayer(M1);TileGroundLayer(M1.5 预留)
|
||||||
|
│ ├─ view/ # QtWidgets 视图(被动;持有 VTK widget 外壳,不 new actor)
|
||||||
|
│ │ ├─ login/ # LoginWindow(样式参考 web)
|
||||||
|
│ │ ├─ panels/ # ObjectTreePanel, DatasetListPanel, MapViewPanel(QVTKOpenGLStereoWidget),
|
||||||
|
│ │ │ # DataDetailPanel, AnomalyPanel, ObjectPropertyPanel, PropertyPanel
|
||||||
|
│ │ └─ widgets/ # ColorScaleEditor, ToolbarBits
|
||||||
|
│ ├─ controller/ # 联动编排(按交互闭环拆分,避免 God Object)
|
||||||
|
│ │ ├─ SelectionController # 勾选/选中状态
|
||||||
|
│ │ ├─ RenderSyncController # 状态→Scene 渲染同步
|
||||||
|
│ │ └─ DetailSyncController # 列表↔详情↔视图定位三向联动
|
||||||
|
│ └─ app/ # main / MainWindow(ADS 布局、主题)/ AppContext(DI 根)
|
||||||
|
├─ resources/ # QSS 主题、QtAwesome、登录页素材
|
||||||
|
├─ tests/ # gtest(core/data/algo)+ Qt Test(view/controller)
|
||||||
|
└─ docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
**架构铁律(写入 .clangd 供 AI 读取)**:
|
||||||
|
|
||||||
|
- `core` 绝不 `#include` 任何 Qt / VTK 头(含 `IInterpolator`,返回 `core::ScalarVolume`)。
|
||||||
|
- VTK actor / RenderWindow 一律由 `render` 层创建与持有;`view` 只持有 `QVTKOpenGLStereoWidget` 外壳,把其 interactor 注入 render,并将**拾取/交互事件回流**给 controller(见 §4.4),禁止直接 new actor。
|
||||||
|
- 数据流双向已显式化:`view → render`(交互注入)与 `render → controller`(拾取/选择出站信号)。
|
||||||
|
- 信号槽连接集中在各 `*Controller` / `MainWindow` 的 `wireUp()`。
|
||||||
|
- 所有落盘路径经 `QStandardPaths`(规约 §7.1)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 渲染核心:单一 VTK 场景(K-1)
|
||||||
|
|
||||||
|
> **⚠️ 实现修正(2026-06-07,经离屏 PNG 核对;权威做法以此为准,详见 `plans/2026-06-07-m1-view-redesign.md` + STATUS)**
|
||||||
|
> 本节 §4.2「2D/3D 仅切相机预设、零数据重建」的理想对**当前 M1 测线数据不成立**:剖面是竖直帘面,俯视只剩一条发丝线 → 俯视图空白。M1 落地做法:
|
||||||
|
> - **二维地图** 与 **三维视图** 是**两种不同渲染内容**(非同一物体换相机):二维地图 = 测线 `lat/lon` 轨迹**线**(`MapLineActor`,俯视);三维视图 = 沿测线的**竖直帘面**(`CurtainActor`,z 取负、纵向夸张、分段色带)。
|
||||||
|
> - **数据详情**(独立 QVTK)才显示单条数据集的 **#18 平面反演剖面**(`GridContourActor`,y 取负、显式 structuredGrid、colorBar 真实分段值、纵向夸张)。
|
||||||
|
> - 坐标统一用 `core::GeoLocalFrame`(经纬→局部米)。**dd_voxel/dd_slice 搁置**(散点 projX/Y 真实 CRS 未确认,无法与 lat/lon 配准)。
|
||||||
|
> - K-1「单场景 + 相机预设」仍是**长期目标**,但需要从俯视/透视都可读的内容(如带底图的地面 + 测线落地线 + 帘面共存)才成立。
|
||||||
|
> - **等值线/体素着色必须用 colorBar 真实非均匀分段值**(均匀分级会一片蓝)。**渲染改动必须用 `tests/spike/render_verify.cpp` 离屏 PNG 核对**。
|
||||||
|
|
||||||
|
### 4.1 Scene 与 RenderWindow 所有权
|
||||||
|
|
||||||
|
- `render::Scene` 持有**唯一** `vtkRenderWindow` + `vtkRenderer` + 项目世界坐标空间里的全部 actor,维护当前色阶与坐标系。
|
||||||
|
- **单一 `QVTKOpenGLStereoWidget`**(K-9,QOpenGLWidget 系,FBO 合成、reparent 友好)承载渲染窗口,**不放进 Tab**;中央面板的「二维/三维」是工具栏上的模式切换,不是两个 widget。
|
||||||
|
- view 仅持有该 widget 外壳;RenderWindow/Interactor 所有权归 render。
|
||||||
|
|
||||||
|
### 4.2 2D / 3D = 三要素组合
|
||||||
|
|
||||||
|
| 模式 | CameraPreset | InteractorStyle | 典型可见性 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 二维 | Top2D:正交投影、俯视、Z 锁定 | Locked2D:禁旋转,平移/缩放/正南正北 | 地面 + 平面要素 + 俯视散点/网格 |
|
||||||
|
| 三维 | Free3D:透视、自由轨道 | Orbit3D:自由旋转/缩放/平移 | 全部 actor(体素、剖面、地形起伏) |
|
||||||
|
|
||||||
|
切换 = 切相机预设 + 交互器样式 + 工具集 + actor 可见性,**零数据重建**。
|
||||||
|
|
||||||
|
### 4.3 数据 → VTK 管线映射(已按评审修正)
|
||||||
|
|
||||||
|
| 数据类型 | 来源 | VTK 管线(修正后) | 备注 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 剖面散点 | 剖面原数据(2597 点) | `vtkPolyData`(verts) + `vtkLookupTable` 着色 | 图 #17 |
|
||||||
|
| 网格等值面/线/标注 | 网格数据(规则栅格 x[100]×y[22],v[22][100],z 抬升) | **`vtkImageData`(origin+spacing)→(z 抬升用 `vtkWarpScalar`)→ `vtkDataSetSurfaceFilter`/`vtkGeometryFilter` → `vtkBandedPolyDataContourFilter`(开 `GenerateContourEdgesOn()` 一次产 banded 面+等值线,共用阈值)→ `vtkLabeledDataMapper` 标注** | 图 #18;**不可让 structured/image 直连 banded filter**(B-1) |
|
||||||
|
| dd_voxel 体绘制 | 多剖面散点 → `IInterpolator` → `core::ScalarVolume` | `ScalarVolume` →(render 转)`vtkImageData` → `vtkGPUVolumeRayCastMapper` + 颜色/不透明度传递函数 | 图 #09;插值域受限(§10) |
|
||||||
|
| dd_slice 切片 | voxel + 受控切面 | **`vtkResliceCursorWidget` / `vtkImageReslice`**(受控正交/任意切片),随相机模式启停 | 替代 `vtkImagePlaneWidget`(避免与交互器抢事件,M-2) |
|
||||||
|
| 异常圈定 | 异常数据(markType 1点/2线/3面 + legend + z/elevation) | **按 markType 三条子管线**:点 `vtkGlyph3D`(pointShape)、线 polydata+dashed、面 `vtkPolygon`/`vtkTriangleFilter` 填充+边框;标注屏幕空间 billboard | legend 的 `*NoOpacity` 0–100 → 归一 [0,1];z 取值同剖面 Z 基准(§5) |
|
||||||
|
| DEM 地形 + 影像 | dem.tif + image.tif + tfw(**可能异源 CRS**) | GDAL 读 → **PROJ/GDAL 重投影到项目世界 CRS** → `vtkImageData` → `vtkWarpScalar` 抬升 + 影像纹理 | 图 #05;影像实测为 EPSG:3857,须重投影(§5、M-1) |
|
||||||
|
| 色阶 | colorBar:[值, 颜色] 阶梯 | `vtkLookupTable`(离散阶梯,取下界)+ `vtkScalarBarActor` | 见 §7 |
|
||||||
|
|
||||||
|
### 4.4 模态交互与拾取回流(M-2、B-3)
|
||||||
|
|
||||||
|
- `InteractionManager` 管理**模态工具**激活互斥与 VTK observer 优先级:`MeasureTool`、`SliceTool`、`PickSelectTool`。工具激活/退出负责其临时 actor 生命周期。
|
||||||
|
- 3D Widget(切片)与自定义 InteractorStyle 共享同一 interactor,须显式管理 `SetEnabled()` 与事件优先级,避免抢事件。
|
||||||
|
- **拾取回流通道**:`render` 拾取到对象 → 经 view 中转发出出站信号 → `DetailSyncController` → 列表/详情定位。此箭头在分层图中显式存在(§3)。
|
||||||
|
|
||||||
|
### 4.5 GroundLayer 可插拔
|
||||||
|
|
||||||
|
`IGroundLayer { build(Scene&); setVisible(bool); }`:M1 `DemImageGroundLayer`;M1.5 `TileGroundLayer`。若 VTK 贴瓦片体验差(D-5),可仅替换二维为 MapLibre 而不动 data/render 的 actor 体系。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 坐标系设计(K-7,评审最大短板,已重写)
|
||||||
|
|
||||||
|
数据现实(已核验真实样本):
|
||||||
|
|
||||||
|
- 剖面/网格/异常:带 GIS 投影 `projectX`≈516868=**Easting**、`projectY`≈2494259=**Northing**;另带局部米 `xlist/ylist`(各数据集自原点起算)。
|
||||||
|
- ⚠️ **CRS 待确认(Phase 1 用 PROJ 实测纠正)**:`projectX/Y` 的真实 CRS **不是 EPSG:32649**。PROJ 实测 `(516868,2494259)` 在 EPSG:32649 下解出 lon≈**111.16°E**,而网格自带 lat/lon 是 **114.16°E**(docx 标明为**香港** Volia 数据,香港≈114°E)——真实 CRS 的中央经线在 ~114°E(疑为港式/自定义 TM)。**做底图/影像配准(M1.5)前必须向客户确认项目 CRS**。
|
||||||
|
- **对 M1 core 无影响**:`LocalFrame` 用相对米(减原点,CRS 无关),网格自带 lat/lon;`CrsTransform` 已实现并单测验证 PROJ 机制本身。
|
||||||
|
- 影像 `image.tfw`:原点 (12708343, 2577685) = **EPSG:3857(Web 墨卡托)**,与剖面**不同投影**。
|
||||||
|
- 网格另带 `elevation[100]` / `lat/lon`(经纬度,EPSG:4326)。
|
||||||
|
- API 几何 `tm/geometry/get` 返回 **EPSG:4326**。
|
||||||
|
|
||||||
|
**设计**:
|
||||||
|
|
||||||
|
1. **唯一权威系 = 项目世界系**:局部米,含明确 Z 基准;选定一个工作平面 CRS(默认项目 UTM,如 EPSG:32649)+ 双精度原点偏移。
|
||||||
|
2. **每数据源各记源 CRS + 源 LocalFrame**:领域模型为每个数据集保存其源 CRS 与(如有)自身局部原点。**不假设全项目单一 CRS**。
|
||||||
|
3. **统一 rebase 管线**(显式步骤,非一句话):任何几何进入 Scene 前,`CrsTransform`(PROJ)把 `源局部米 →(源原点)→ 源 CRS GIS → 项目世界 CRS →(减项目原点)→ 项目世界米`。多数据集因此对齐到同一世界系(解决 B-1 多原点冲突)。
|
||||||
|
4. **轴向约定钉死**:world.x = Easting = `projectX`,world.y = Northing = `projectY`,world.z 向上为正、单位米。**解析器不信 `eastCoord/northCoord` 字段名**(实测与值颠倒),按 projectX/Y 取值,单测对照(B-2)。
|
||||||
|
5. **垂向(Z)基准统一**(M-3):`LocalFrame` 定义高程基准面、向上为正、单位米、可选垂向夸张 z-scale。网格 z(剖面深度/构造面)、DEM `elevation`(地表高程)、体素 Z 在进入 Scene 前统一归算到该基准,避免地形与剖面垂直穿插。
|
||||||
|
6. **影像/DEM 重投影**:装载时经 GDAL/PROJ 重投影到项目世界 CRS 再贴地,**不能简单减原点**(M-1)。
|
||||||
|
7. **float 精度**:世界=局部米(小数值)从根本规避 VTK float 大坐标抖动。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 数据层:Repository(K-4,异步契约)
|
||||||
|
|
||||||
|
接口即按 **API 现实形态**定义(异步 + 分页 + 取消),本地实现用 QtConcurrent 满足同一签名(M-1):
|
||||||
|
|
||||||
|
```
|
||||||
|
IProjectRepository {
|
||||||
|
QFuture<Project> loadProject(id);
|
||||||
|
QFuture<vector<GsObject>> loadStructure(projectId); // GS/TM 树
|
||||||
|
}
|
||||||
|
IDatasetRepository {
|
||||||
|
QFuture<Page<DsObject>> listDatasets(tmObjectId, PageReq); // 分页
|
||||||
|
QFuture<ScatterField> loadScatter(dsId);
|
||||||
|
QFuture<Grid> loadGrid(dsId);
|
||||||
|
QFuture<ColorScale> loadColorScale(dsId);
|
||||||
|
QFuture<vector<Anomaly>> loadAnomalies(dsId);
|
||||||
|
QFuture<TerrainTile> loadTerrain(...);
|
||||||
|
// 大数据(体素/雷达):返回带取消句柄 + 进度回调;M1.5 走 FlatBuffers/Protobuf 流
|
||||||
|
RequestHandle loadVolumeStream(dsId, sink, onProgress); // 可 cancel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 切换 ds 时取消上一个未完成请求;列表类带游标/分页;大数据流式 + 进度 + 取消。
|
||||||
|
- **M1**:`LocalSampleRepository` 读样本目录,解析器映射成领域模型(DTO ↔ model 在 `data/dto` 隔离)。
|
||||||
|
- **未来**:`ApiRepository` 同签名对接 `pop-api`。
|
||||||
|
|
||||||
|
### 6.1 样本文件 → 模型解析约定(已核对真实样本)
|
||||||
|
|
||||||
|
| 文件 | 结构 | 解析要点 |
|
||||||
|
|---|---|---|
|
||||||
|
| 剖面原数据N.txt | `{data:{min,max,projectXList,projectYList,vlist,xlist,ylist,hlist}}` | 2597 点;local(x,y)+gis(projX=East,projY=North)+value |
|
||||||
|
| 剖面网格数据N.txt | `{data:{x[100],y[22],v[22][100],z[22][100],elevation[100],lat[100],lon[100],vmin,vmax,overlayCoordinate,overlayElevation}}` | **规则栅格**(dx≈0.709,dy≈0.704 恒定)→ vtkImageData;**v/z 为 [j=y][i=x],灌点序 i 最快**;无顶层 min/max;对未知字段宽容 |
|
||||||
|
| 剖面网格数据的色阶数据N.txt | `{data:{properties:{colorBar:[[值,rgba]],lineConfig,labelConfig,lvlMinMax}}}` | 17 段阶梯;`lineType` 实测 "solid"(以配置为准,勿硬编码 dashed) |
|
||||||
|
| 剖面网格数据N——对应的异常圈定数据.txt | `{data:[{exceptionName,exceptionMarkType(1点/2线/3面),legend{point*/polyline*/polygon*},location:{coordinate[{x,y}]},zlist?,elevationList?,geographicalCoordinates{projectX,projectY,...}}]}` | 字段比早期列举多;`eastCoord/northCoord` 名值颠倒,按 projectX/Y 取 |
|
||||||
|
| dem.tif / image.tif / image.tfw | GeoTIFF + world file | **影像 tfw 为 EPSG:3857**;GDAL 读 + PROJ 重投影到世界 CRS |
|
||||||
|
| test_001_A*.head/.data/.cor | GPR 原始(462×4100×int16,多通道分文件) | **属 M1.5 雷达,LocalSampleRepository 不解析** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 色阶(ColorScale)
|
||||||
|
|
||||||
|
`colorBar` 为 `[值, 颜色]` 阶梯数组(颜色支持 `#RRGGBB` 与 `rgba(r,g,b,a)`)。映射:值落相邻两 stop 间取**下界 stop** 颜色(阶梯,非线性插值)。
|
||||||
|
|
||||||
|
- **实现统一为离散 `vtkLookupTable`**(贴合「取下界阶梯」语义,2D/3D 共用同一可信源),显式定义 under(低于首 stop)/ over(高于末 stop)/ NaN 颜色。
|
||||||
|
- **alpha 量纲按色阶来源文件类型判定**(网格色阶 0–255、LVL 色阶 0–1),解析器入口带 source 标记,**不按数值范围猜**(m-2)。
|
||||||
|
- `lineConfig`:等值线显隐/颜色/`lineType`(以配置为准)/zmin/zmax;`labelConfig`:标注显隐/颜色;`equalAreaLayerCount`/`logLinesCount`。
|
||||||
|
- 视图层 `ColorScaleEditor`:M1 读取与基本调整;命名保存对接后续色阶模板 API。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 登录与网络层(M1 必做,真实流程已抓取)
|
||||||
|
|
||||||
|
### 8.1 已确认的生产实现细节
|
||||||
|
|
||||||
|
- **API 基址** `http://tenant.geomative.cn/pop-api`(openresty 反代;OpenAPI 的 `/admin/*`、`/business/*` 加 `/pop-api` 前缀)。
|
||||||
|
- **认证头** `geomativeauthorization: Geomative <token>`(不透明会话令牌,非 JWT)。
|
||||||
|
- **登录三步**:① `GET /business/system/personalUser/getImageCode`→验证码图+`codeId` → ② `POST /business/system/personalUser/verifyCodeCheck {code,codeId}` → ③ `POST /admin/tenant/auth/login2 {username, password=RSA加密, checkCode}`→token。
|
||||||
|
- **密码加密 = JSEncrypt RSA-2048**(前端 vendor 用 JSEncrypt 库;密文 base64 ~344 字符 = 256 字节)。token 取响应 **`data.accessToken`**(值即 `"Geomative <hash>"`,存 web localStorage `token`)。
|
||||||
|
- 另有 `/email`、`/phone` 登录支线(非 M1)。
|
||||||
|
- 登录后:`getInfo` / `list-menus` / `enterprise/info` / `enterprise/joined/list`。
|
||||||
|
|
||||||
|
### 8.2 实现要点
|
||||||
|
|
||||||
|
- `AuthService`:取验证码→展示→校验→**OpenSSL RSA** 加密密码→login2→持有 token。
|
||||||
|
- `Credential`(**QtKeychain**):token 存平台密钥库,严禁明文(规约 §7.4)。
|
||||||
|
- `ApiClient`:注入 `geomativeauthorization`、基址、超时、错误码、401 处理;QtNetwork 原生。
|
||||||
|
- **登录窗 UI**:样式参考现有 web 登录页(实现阶段截图复刻)。
|
||||||
|
|
||||||
|
### 8.3 ⚠️ 前置确认项(与 RSA 同级,M-5)
|
||||||
|
|
||||||
|
抓取的真实流程里**未见 refresh-token 实际使用,login2 只返不透明会话 token**。因此:
|
||||||
|
|
||||||
|
- **RSA 公钥已取得 ✅**(Phase 3,用 Playwright `page.route` 拦截 JS chunk 给 `setPublicKey` 注入 hook + 缓存绕过强制加载补丁版,触发一次真登录捕获)。RSA-2048 SPKI,存于 `resources/rsa_public_key.pem`。加密用 PKCS#1 v1.5(JSEncrypt 默认),`RsaEncryptor`(OpenSSL)已实现+单测。
|
||||||
|
- **token 生命周期 / 是否有 refresh 机制**待确认。据此二选一设计:
|
||||||
|
- (a) 有 refresh token → 标准静默刷新、401 静默续期。
|
||||||
|
- (b) 仅会话 token → 「免登录」= 持久化会话 token 至其有效期;**到期/401 引导用户重新登录(含验证码),不声称静默重登**。
|
||||||
|
- 本项在 spike/实现前向后端确认;spec 不把「静默刷新」当既定能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. UI 外壳:完整工作台(K-3)
|
||||||
|
|
||||||
|
- **停靠框架**:ADS(LGPL,规约 §6.2)。**VTK 面板默认不可浮动**(或浮动时占位、停靠回重建),缓解 reparent 上下文问题(spike 验证,M-4)。
|
||||||
|
- **三区布局**(还原原型):左(对象树 + 数据集列表)/ 中(2D-3D 视图 + 数据详情)/ 右(异常-对象属性 + 属性)。
|
||||||
|
- **主题**:QSS + QDarkStyleSheet 打底 + QtAwesome 图标。
|
||||||
|
- **布局持久化**:ADS 透视图 + 窗口几何存 QSettings(Windows 强制 INI,规约 §7.2)。
|
||||||
|
- **联动**(controller 按闭环拆分,§3):勾选 GS/TM→按 dd 类型筛选 ds→勾选 ds→渲染;列表↔详情↔视图定位三向;色阶调整两视角实时更新。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 算法:展示期三维插值(K-6、K-10)
|
||||||
|
|
||||||
|
- `core/algo/IInterpolator`:`core::ScalarVolume interpolate(const PointSet& pts, const GridSpec& spec)`——**返回 core 中立类型**(dims/spacing/origin/double 数组),绝不含 VTK(M-2)。render 层 `VoxelVolumeActor` 把 `ScalarVolume`→`vtkImageData`。
|
||||||
|
- M1 实现 `IdwInterpolator`(反距离加权,Eigen 辅助;2597 点规模**不需要 PCL/KD-tree**,m-1)。
|
||||||
|
- **可信度与数据依赖(K-10、B-3)**:可信体素需 **≥3 条非共线剖面或真实 3D 网格(dd_Property3D)/ 反演网格** 输入。仅两条近平行剖面 IDW 会得到「夹层片状」幻影。故:
|
||||||
|
- 插值**限定在输入包络内 + 最大距离 clamp**,包络外置 blank/透明,避免 ray cast 渲染整盒幻影。
|
||||||
|
- M1 体绘制按 §13 分阶段:先在**充分输入**数据上出可信体;输入不足的复杂体后置。
|
||||||
|
- **数据依赖**:需客户提供达到可信度的体素级输入数据(≥3 剖面 / 3D 网格)——列入 §14 待办。
|
||||||
|
- **不做反演**(上游、Python 生态 ResIPy 等),未来按规约 §8.3 进程隔离接入,M1 仅接口预留。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 构建与依赖(K-8,方案②-修订:官方 MSVC Qt + 源码 VTK + vcpkg 非 Qt 依赖)
|
||||||
|
|
||||||
|
- **构建**:CMake 3.21+。MSVC 工具集 **VS18 / 14.51**(实机),C++17;生成 `compile_commands.json`。
|
||||||
|
- **单一 Qt 纪律(核心)**:全链路只用**一份官方 MSVC 预编译 Qt**(`D:\Qt\6.11.1\msvc2022_64`,经 `CMAKE_PREFIX_PATH`)。**凡依赖 Qt 的组件都不走 vcpkg**(vcpkg 任何 Qt 依赖端口都会再编一份 qtbase = 双份冲突,已核 `ports/vtk/vcpkg.json`:`vtk[qt]`→`qtbase`+`qtdeclarative`)。
|
||||||
|
- **VTK**:无 MSVC 预编译,**必须源码编**。预先用官方 Qt 把 VTK 9.3 配置/编译/`install` 到 `external/vtk-install`(`-DVTK_GROUP_ENABLE_Qt=YES -DQt6_DIR=...`),app 经 `VTK_DIR` `find_package(VTK)`。一次编好、隔离于 app 构建。
|
||||||
|
- **ADS / QtKeychain**:经 **FetchContent** 对接同一份官方 Qt(体量小,源码编可接受),**不走 vcpkg**。
|
||||||
|
- **非 Qt 依赖经 vcpkg**:GDAL/PROJ/OpenSSL/Eigen/spdlog/fmt/nlohmann-json/gtest(这些不拉 Qt)。
|
||||||
|
- **M1 依赖矩阵**:
|
||||||
|
|
||||||
|
| 依赖 | 来源 | 用途 | 许可证 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Qt 6.11.1(msvc2022_64,预编译) | 官方安装器(MSVC kit) | UI/网络/SQL/并发 | LGPLv3(动态)⚠️ 商务 D-2 |
|
||||||
|
| VTK 9.3([qt,opengl] + gdal/proj 可选) | **源码编→install 前缀** | 三维渲染 + QVTK widget | BSD ✅ |
|
||||||
|
| ADS(Qt-Advanced-Docking-System) | FetchContent(对接官方 Qt) | 停靠布局 | LGPL v2.1 ✅ |
|
||||||
|
| QtKeychain | FetchContent(对接官方 Qt) | 凭证存储 | BSD ✅ |
|
||||||
|
| gdal / proj | vcpkg | DEM/影像/坐标重投影 | MIT 类 ✅ |
|
||||||
|
| openssl | vcpkg | RSA/HTTPS | Apache 2.0 ✅ |
|
||||||
|
| eigen3 | vcpkg(头文件) | 数值/插值 | MPL2 ✅ |
|
||||||
|
| spdlog / fmt | vcpkg | 日志 | MIT ✅ |
|
||||||
|
| nlohmann-json | vcpkg(头文件) | JSON | MIT ✅ |
|
||||||
|
| gtest | vcpkg | 单测 | BSD ✅ |
|
||||||
|
|
||||||
|
- ~~PCL~~:**M1 移除**(点规模不需要)。
|
||||||
|
- **ABI**:官方 Qt 为 MSVC 2022(v143)预编译,本机 VS18(14.51)编 VTK/app/ADS/QtKeychain;"新链旧"在 MSVC v14x 兼容区内**安全**,全程动态 CRT `/MD[d]`、Release 链 Release、Debug 链 Debug,不跨配置混链。
|
||||||
|
- 部署:用**官方 Qt 的 `windeployqt`**(`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe`)部署 Qt + 插件;VTK/vcpkg dll 用 `TARGET_RUNTIME_DLLS` 拷贝。确保 exe 目录只有这一份 Qt。
|
||||||
|
- **二进制缓存**:vcpkg 实测无缓存,落地前先配 `VCPKG_BINARY_SOURCES`(省 GDAL/PROJ 等重编)。
|
||||||
|
- 环境从零搭建:见 `docs/ENV_SETUP_Windows.md`(方案②-修订)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 测试策略(规约 §10.2)
|
||||||
|
|
||||||
|
- `core`/`data`/`algo`:gtest(坐标 rebase/轴向、Z 基准归算、colorBar LUT 映射、v[j][i] 灌点序、样本解析、IDW 正确性)。
|
||||||
|
- `view`/`controller`:Qt Test(联动、模态工具互斥)。
|
||||||
|
- 失败路径必测:登录失败/验证码错/token 失效、样本缺失/损坏、空数据集、异源 CRS 重投影。
|
||||||
|
- 渲染以小基准截图人工核对(对照图 #17/#18/#09)。
|
||||||
|
- clang-tidy + cppcheck 入 CI;Debug 启用 ASan/UBSan。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. M1 验收标准(图分阶段,K-5/K-10)
|
||||||
|
|
||||||
|
**M1-a(先达成)**
|
||||||
|
1. 启动→登录窗(近 web)→真连 `pop-api` 登录成功。
|
||||||
|
2. 完整工作台 ADS 三区停靠;对象树/数据集来自本地样本。
|
||||||
|
3. ① 渲染剖面散点(图 #17 样)、网格 banded 等值面+等值线+标注(图 #18 样)、异常按 markType 圈定。
|
||||||
|
4. ④ DEM 地形起伏 + 影像(经重投影对齐)。
|
||||||
|
5. 色阶离散 LUT 可调,两视角实时联动;二维俯视相机预设可切。
|
||||||
|
|
||||||
|
**M1-b(在充分输入数据上达成可信体)**
|
||||||
|
6. 由 ≥3 剖面/3D 网格经 IDW 生成**可信** dd_voxel 体绘制,插值域受限;`vtkResliceCursorWidget` 交互切片得 dd_slice。
|
||||||
|
|
||||||
|
**通用**:core 单测通过;clang-tidy 无新增告警;布局/偏好持久化生效;2D/3D 切换零数据重建、坐标对齐正确。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 风险与待决(承接规约 §11)
|
||||||
|
|
||||||
|
| 风险/待决 | 说明 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| D-2 Qt 许可证 | LGPL 动态 vs 商业 | M1 按 LGPL 动态链接;商务并行 |
|
||||||
|
| RSA 公钥来源 + token 生命周期 | 登录加密公钥、是否有 refresh | spike 前向后端确认(§8.3) |
|
||||||
|
| 可信体输入数据 | 2 平行剖面不足以出可信体(K-10) | 需客户提供 ≥3 剖面/3D 网格;M1-b 验收依赖此 |
|
||||||
|
| ADS 端口 + QVTK reparent | vcpkg 端口可用性、浮动黑屏 | spike 门槛验证(§15) |
|
||||||
|
| 全 vcpkg 首编译耗时 | VTK+Qt 编译久 | 二进制缓存 `VCPKG_BINARY_SOURCES` |
|
||||||
|
| 异源 CRS 配准 | 影像 3857 vs 剖面 32649 | GDAL/PROJ 重投影(§5) |
|
||||||
|
| macOS / OpenGL 废弃 | 规约 §3.3 | M1 仅 Windows;保持可移植 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Spike 预研门槛(K-2,进入完整实现计划前必过)
|
||||||
|
|
||||||
|
第一周先跑通三个高风险点,任一不过则调整方案后再展开计划:
|
||||||
|
|
||||||
|
1. **构建/部署 spike**:全 vcpkg(qtbase + vtk[qt] 共用一份 Qt)配置、编译、出 exe、单一链路部署、无双 Qt 冲突。
|
||||||
|
2. **UI 上下文 spike**:ADS(vcpkg 或 FetchContent)+ `QVTKOpenGLStereoWidget`,验证停靠/浮动/重停靠不黑屏、相机预设切换稳定。
|
||||||
|
3. **渲染管线 spike**:用真实样本跑通 `vtkImageData(+warp) → geometry filter → vtkBandedPolyDataContourFilter(GenerateContourEdges) → 标注`,目视对照图 #18;散点 + 离散 LUT 色阶对照图 #17。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. v2 修订记录(对应评审)
|
||||||
|
|
||||||
|
- 网格管线改 `vtkImageData(+warp)→geometry filter→banded contour(GenerateContourEdges)`(B-1/B-2 code)。
|
||||||
|
- 坐标系重写:多源 CRS + 各自 LocalFrame + 统一 rebase + 轴向钉死 + Z 基准 + 影像重投影(B-1/B-2 arch、M-1/M-3 code、M-3 arch)。
|
||||||
|
- dd_voxel 维持可信体但列数据依赖、插值域受限、验收分阶段(B-3 code、K-10)。
|
||||||
|
- Repository 改异步契约(分页/取消/流)(M-1 arch)。
|
||||||
|
- `IInterpolator` 返回 core `ScalarVolume`,去 VTK(M-2 arch)。
|
||||||
|
- 交互:模态工具抽象 + 拾取回流 + 切片改 `vtkResliceCursorWidget`(B-3 arch、M-2 code)。
|
||||||
|
- widget 改 `QVTKOpenGLStereoWidget` + VTK 面板不可浮动 + spike(M-4 code)。
|
||||||
|
- 构建改全 vcpkg(已核 vtk[qt]→qtbase 依赖)、删 PCL(M-5 code、m-1)。
|
||||||
|
- 登录 refresh/token 生命周期降为前置确认(M-5 arch)。
|
||||||
|
- 色阶离散 LUT + under/over/NaN + alpha 按来源 + lineType 以配置为准(m-1/m-2/m-4 各)。
|
||||||
|
- controller 拆 Selection/RenderSync/DetailSync(m-5 arch)。
|
||||||
|
- 新增 §15 spike 门槛(K-2)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*v2 经双专家评审 + 数据核验修订。下一步:spike 预研 → writing-plans。*
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
对象:项目/gs/tm对象,鼠标右键,tm导入ds,编辑在右侧面板直接做;gs也有导入ds功能;
|
||||||
|
|
||||||
|
数据集:单击更新属性,双击时打开或选中数据详情,选中三维视图;文件列表,双击按支持的文件格式预览,不支持的文件格式提示+下载;
|
||||||
|
|
||||||
|
二维三维视图:对象视图中选中的TM,需要使用树形列表展示可渲染图形的tm/ds树,并支持选中或取消某个tm/ds,决定是否渲染;展示异常;鼠标点选图形时,如果在垂直屏幕方向有多个图形叠加,需要把多个图形的信息列出来,方便用户选中被遮住的图形;
|
||||||
|
|
||||||
|
数据详情:
|
||||||
|
|
||||||
|
异常:gs选择后,展示下面的合集;异常体不渲染;通过类型或状态过滤;支持多选异常创建异常体,支持将异常拖入异常体;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
项目列表:新增项目,最后访问时间倒序;项目列表可切换地图模式;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
分析视图、大屏视图(web):按项目类型设置绑定视图,客户端可切换,分析视图默认;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
个人资料:显示基本信息、修改邮箱、修改密码;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
大文件上传(ds中的地形文件)需要走独立的接口;
|
||||||
|
|
@ -11,16 +11,22 @@ find_package(VTK REQUIRED COMPONENTS
|
||||||
FiltersModeling
|
FiltersModeling
|
||||||
)
|
)
|
||||||
find_package(nlohmann_json CONFIG REQUIRED)
|
find_package(nlohmann_json CONFIG REQUIRED)
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Svg)
|
||||||
|
|
||||||
add_executable(geopro_desktop WIN32
|
add_executable(geopro_desktop WIN32
|
||||||
main.cpp
|
main.cpp
|
||||||
|
Theme.cpp
|
||||||
|
TopBar.cpp
|
||||||
|
Glyphs.cpp
|
||||||
|
PanelHeader.cpp
|
||||||
login/LoginWindow.cpp
|
login/LoginWindow.cpp
|
||||||
panels/AnomalyListPanel.cpp)
|
panels/AnomalyListPanel.cpp
|
||||||
|
panels/DatasetListPanel.cpp)
|
||||||
|
|
||||||
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(geopro_desktop PRIVATE
|
target_link_libraries(geopro_desktop PRIVATE
|
||||||
Qt6::Core Qt6::Gui Qt6::Widgets
|
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg
|
||||||
${VTK_LIBRARIES}
|
${VTK_LIBRARIES}
|
||||||
ads::qt6advanceddocking
|
ads::qt6advanceddocking
|
||||||
nlohmann_json::nlohmann_json
|
nlohmann_json::nlohmann_json
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPen>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QString>
|
||||||
|
#include <QSvgRenderer>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 每个图标的 SVG 内容(线性风格,24x24 视窗,描边由外层注入颜色)。
|
||||||
|
// 路径取自 Lucide 图标集(MIT 许可,业界通用),保证专业、清晰、语义正确。
|
||||||
|
QString svgPathFor(Glyph t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case Glyph::Tree:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M21 12h-8'/><path d='M21 6H8'/><path d='M21 18h-8'/>"
|
||||||
|
"<path d='M3 6v4c0 1.1.9 2 2 2h3'/><path d='M3 10v6c0 1.1.9 2 2 2h3'/>");
|
||||||
|
case Glyph::Dataset:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<ellipse cx='12' cy='5' rx='9' ry='3'/>"
|
||||||
|
"<path d='M3 5v14a9 3 0 0 0 18 0V5'/><path d='M3 12a9 3 0 0 0 18 0'/>");
|
||||||
|
case Glyph::Map:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 "
|
||||||
|
"0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z'/>"
|
||||||
|
"<path d='m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65'/>"
|
||||||
|
"<path d='m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65'/>");
|
||||||
|
case Glyph::Detail:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M3 3v18h18'/><path d='M18 17V9'/><path d='M13 17V5'/><path d='M8 17v-3'/>");
|
||||||
|
case Glyph::Anomaly:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 "
|
||||||
|
"1.73-3Z'/><path d='M12 9v4'/><path d='M12 17h.01'/>");
|
||||||
|
case Glyph::Property:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<circle cx='12' cy='12' r='10'/><path d='M12 16v-4'/><path d='M12 8h.01'/>");
|
||||||
|
case Glyph::Plus:
|
||||||
|
return QStringLiteral("<path d='M5 12h14'/><path d='M12 5v14'/>");
|
||||||
|
case Glyph::Filter:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<polygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/>");
|
||||||
|
case Glyph::Upload:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/>"
|
||||||
|
"<path d='M17 8l-5-5-5 5'/><path d='M12 3v12'/>");
|
||||||
|
case Glyph::Download:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/>"
|
||||||
|
"<path d='M7 10l5 5 5-5'/><path d='M12 15V3'/>");
|
||||||
|
case Glyph::Collapse:
|
||||||
|
return QStringLiteral("<path d='m17 11-5-5-5 5'/><path d='m17 18-5-5-5 5'/>");
|
||||||
|
case Glyph::Workspace:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<rect width='7' height='7' x='3' y='3' rx='1'/>"
|
||||||
|
"<rect width='7' height='7' x='14' y='3' rx='1'/>"
|
||||||
|
"<rect width='7' height='7' x='14' y='14' rx='1'/>"
|
||||||
|
"<rect width='7' height='7' x='3' y='14' rx='1'/>");
|
||||||
|
case Glyph::Folder:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 "
|
||||||
|
"0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'/>");
|
||||||
|
case Glyph::Help:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<circle cx='12' cy='12' r='10'/>"
|
||||||
|
"<path d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'/><path d='M12 17h.01'/>");
|
||||||
|
case Glyph::Bell:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9'/>"
|
||||||
|
"<path d='M10.3 21a1.94 1.94 0 0 0 3.4 0'/>");
|
||||||
|
case Glyph::Gear:
|
||||||
|
return QStringLiteral(
|
||||||
|
"<path d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 "
|
||||||
|
"0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 "
|
||||||
|
"1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73 "
|
||||||
|
".73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 "
|
||||||
|
"2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 "
|
||||||
|
"2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 "
|
||||||
|
".73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 "
|
||||||
|
"1-1-1.73V4a2 2 0 0 0-2-2z'/><circle cx='12' cy='12' r='3'/>");
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QIcon makeGlyph(Glyph type, const QColor& color, int px)
|
||||||
|
{
|
||||||
|
const QString svg =
|
||||||
|
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
|
||||||
|
"stroke='%1' stroke-width='2' stroke-linecap='round' "
|
||||||
|
"stroke-linejoin='round'>%2</svg>")
|
||||||
|
.arg(color.name(), svgPathFor(type));
|
||||||
|
|
||||||
|
QSvgRenderer renderer(svg.toUtf8());
|
||||||
|
|
||||||
|
// 以 3x 超采样渲染再设 devicePixelRatio,保证在任意缩放/DPI 下都清晰。
|
||||||
|
constexpr qreal kSuper = 3.0;
|
||||||
|
const int dim = qRound(px * kSuper);
|
||||||
|
QImage img(dim, dim, QImage::Format_ARGB32_Premultiplied);
|
||||||
|
img.fill(Qt::transparent);
|
||||||
|
QPainter p(&img);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderer.render(&p, QRectF(0, 0, dim, dim));
|
||||||
|
p.end();
|
||||||
|
|
||||||
|
QPixmap pm = QPixmap::fromImage(img);
|
||||||
|
pm.setDevicePixelRatio(kSuper);
|
||||||
|
return QIcon(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString writeChevronIcon(bool open, const QColor& color)
|
||||||
|
{
|
||||||
|
constexpr int px = 16;
|
||||||
|
constexpr int kScale = 3;
|
||||||
|
QPixmap pm(px * kScale, px * kScale);
|
||||||
|
pm.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter p(&pm);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
QPen pen(color, px * kScale * 0.1);
|
||||||
|
pen.setCapStyle(Qt::RoundCap);
|
||||||
|
pen.setJoinStyle(Qt::RoundJoin);
|
||||||
|
p.setPen(pen);
|
||||||
|
|
||||||
|
const double w = px * kScale;
|
||||||
|
const double h = px * kScale;
|
||||||
|
if (open) { // ▼ 向下
|
||||||
|
p.drawLine(QPointF(w * 0.32, h * 0.42), QPointF(w * 0.5, h * 0.60));
|
||||||
|
p.drawLine(QPointF(w * 0.5, h * 0.60), QPointF(w * 0.68, h * 0.42));
|
||||||
|
} else { // ▶ 向右
|
||||||
|
p.drawLine(QPointF(w * 0.42, h * 0.32), QPointF(w * 0.60, h * 0.5));
|
||||||
|
p.drawLine(QPointF(w * 0.60, h * 0.5), QPointF(w * 0.42, h * 0.68));
|
||||||
|
}
|
||||||
|
p.end();
|
||||||
|
|
||||||
|
const QString path =
|
||||||
|
QDir(QDir::tempPath()).filePath(open ? QStringLiteral("geopro_tree_open.png")
|
||||||
|
: QStringLiteral("geopro_tree_closed.png"));
|
||||||
|
pm.save(path, "PNG");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 程序绘制的扁平线性图标(无需图片资源):用于面板表头与表头操作按钮,
|
||||||
|
// 风格对齐原型(细线、圆角端点、单色随主题)。仅外观,无交互逻辑。
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
enum class Glyph {
|
||||||
|
// 面板表头图标
|
||||||
|
Tree, // 对象显示栏(层级)
|
||||||
|
Dataset, // 数据真实显示栏(数据集/数据库)
|
||||||
|
Map, // 二维/三维视图(图层)
|
||||||
|
Detail, // 数据详情(图表)
|
||||||
|
Anomaly, // 异常列表(警示三角)
|
||||||
|
Property, // 属性(信息)
|
||||||
|
// 表头操作按钮图标
|
||||||
|
Plus, // 新建/添加
|
||||||
|
Filter, // 筛选(漏斗)
|
||||||
|
Upload, // 上传
|
||||||
|
Download, // 导出/下载
|
||||||
|
Collapse, // 折叠(双箭头)
|
||||||
|
// 顶部应用栏图标
|
||||||
|
Workspace, // 工作空间(2x2 宫格)
|
||||||
|
Folder, // 项目(文件夹)
|
||||||
|
Help, // 帮助(?)
|
||||||
|
Bell, // 通知(铃铛)
|
||||||
|
Gear, // 设置(齿轮)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 2x 绘制保证清晰)。
|
||||||
|
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16);
|
||||||
|
|
||||||
|
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
|
||||||
|
// 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。
|
||||||
|
QString writeChevronIcon(bool open, const QColor& color);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
#include "PanelHeader.hpp"
|
||||||
|
|
||||||
|
#include <QButtonGroup>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ── 专业图标/字号尺寸(统一放大)──
|
||||||
|
constexpr int kHeaderHeight = 42;
|
||||||
|
constexpr int kTitleIcon = 20; // 表头标题图标
|
||||||
|
constexpr int kActionIcon = 19; // 表头操作按钮图标
|
||||||
|
constexpr int kTabIcon = 19; // Tab 图标
|
||||||
|
|
||||||
|
// 表头统一样式(标准表头 + Tab 表头共用)。
|
||||||
|
const char* kHeaderQss =
|
||||||
|
"#panelHeader { background:#FFFFFF; border-bottom:1px solid #E6EAF1; }"
|
||||||
|
"#panelTitle { color:#1F2A3D; font-size:14px; font-weight:600; }"
|
||||||
|
"#panelBadge { background:#EAEEF5; color:#5A6B85; border-radius:9px;"
|
||||||
|
" padding:1px 7px; font-size:12px; font-weight:600; }"
|
||||||
|
"QToolButton#panelAction { border:none; border-radius:7px; padding:5px; }"
|
||||||
|
"QToolButton#panelAction:hover { background:#EEF3FB; }"
|
||||||
|
"QToolButton#tabBtn { border:none; border-bottom:2px solid transparent; color:#5A6B85;"
|
||||||
|
" padding:8px 4px; font-size:14px; }"
|
||||||
|
"QToolButton#tabBtn:hover { color:#1F2A3D; }"
|
||||||
|
"QToolButton#tabBtn:checked { color:#1F2A3D; font-weight:600;"
|
||||||
|
" border-bottom:2px solid #2D6CB5; }";
|
||||||
|
|
||||||
|
// 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。
|
||||||
|
QLabel* makeBadge(QWidget* parent)
|
||||||
|
{
|
||||||
|
auto* badge = new QLabel(parent);
|
||||||
|
badge->setObjectName(QStringLiteral("panelBadge"));
|
||||||
|
badge->setAlignment(Qt::AlignCenter);
|
||||||
|
badge->setMinimumWidth(16);
|
||||||
|
badge->setVisible(false);
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表头操作按钮(静态占位)。
|
||||||
|
QToolButton* makeActionButton(QWidget* parent, const HeaderAction& a)
|
||||||
|
{
|
||||||
|
auto* btn = new QToolButton(parent);
|
||||||
|
btn->setObjectName(QStringLiteral("panelAction"));
|
||||||
|
btn->setIcon(makeGlyph(a.first, QColor("#5A6B85"), kActionIcon));
|
||||||
|
btn->setIconSize(QSize(kActionIcon, kActionIcon));
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
btn->setToolTip(a.second + QStringLiteral("(占位)"));
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector<HeaderAction>& actions)
|
||||||
|
{
|
||||||
|
auto* header = new QWidget();
|
||||||
|
header->setObjectName(QStringLiteral("panelHeader"));
|
||||||
|
header->setFixedHeight(kHeaderHeight);
|
||||||
|
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
|
||||||
|
|
||||||
|
auto* lay = new QHBoxLayout(header);
|
||||||
|
lay->setContentsMargins(12, 0, 8, 0);
|
||||||
|
lay->setSpacing(8);
|
||||||
|
|
||||||
|
auto* iconLbl = new QLabel(header);
|
||||||
|
iconLbl->setPixmap(makeGlyph(icon, QColor("#44546B"), kTitleIcon).pixmap(kTitleIcon, kTitleIcon));
|
||||||
|
lay->addWidget(iconLbl);
|
||||||
|
|
||||||
|
auto* titleLbl = new QLabel(title, header);
|
||||||
|
titleLbl->setObjectName(QStringLiteral("panelTitle"));
|
||||||
|
lay->addWidget(titleLbl);
|
||||||
|
|
||||||
|
lay->addWidget(makeBadge(header)); // 默认隐藏,调用方可经 findChild 更新
|
||||||
|
|
||||||
|
lay->addStretch();
|
||||||
|
|
||||||
|
for (const auto& a : actions) lay->addWidget(makeActionButton(header, a));
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<HeaderAction>& actions)
|
||||||
|
{
|
||||||
|
auto* box = new QWidget();
|
||||||
|
auto* v = new QVBoxLayout(box);
|
||||||
|
v->setContentsMargins(0, 0, 0, 0);
|
||||||
|
v->setSpacing(0);
|
||||||
|
|
||||||
|
auto* header = new QWidget(box);
|
||||||
|
header->setObjectName(QStringLiteral("panelHeader"));
|
||||||
|
header->setFixedHeight(kHeaderHeight);
|
||||||
|
header->setStyleSheet(QString::fromUtf8(kHeaderQss));
|
||||||
|
auto* hlay = new QHBoxLayout(header);
|
||||||
|
hlay->setContentsMargins(10, 0, 8, 0);
|
||||||
|
hlay->setSpacing(2);
|
||||||
|
|
||||||
|
auto* stack = new QStackedWidget(box);
|
||||||
|
auto* group = new QButtonGroup(box);
|
||||||
|
group->setExclusive(true);
|
||||||
|
|
||||||
|
TabbedPanel result;
|
||||||
|
result.container = box;
|
||||||
|
|
||||||
|
for (int i = 0; i < tabs.size(); ++i) {
|
||||||
|
const PanelTab& t = tabs[i];
|
||||||
|
auto* btn = new QToolButton(header);
|
||||||
|
btn->setObjectName(QStringLiteral("tabBtn"));
|
||||||
|
btn->setText(t.title);
|
||||||
|
btn->setIcon(makeGlyph(t.icon, QColor("#5A6B85"), kTabIcon));
|
||||||
|
btn->setIconSize(QSize(kTabIcon, kTabIcon));
|
||||||
|
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
group->addButton(btn, i);
|
||||||
|
hlay->addWidget(btn);
|
||||||
|
|
||||||
|
QLabel* badge = nullptr;
|
||||||
|
if (t.hasBadge) {
|
||||||
|
badge = makeBadge(header);
|
||||||
|
hlay->addWidget(badge);
|
||||||
|
}
|
||||||
|
result.badges.append(badge);
|
||||||
|
|
||||||
|
stack->addWidget(t.content);
|
||||||
|
hlay->addSpacing(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
hlay->addStretch();
|
||||||
|
for (const auto& a : actions) hlay->addWidget(makeActionButton(header, a));
|
||||||
|
|
||||||
|
QObject::connect(group, &QButtonGroup::idClicked, stack, &QStackedWidget::setCurrentIndex);
|
||||||
|
if (auto* first = group->button(0)) first->setChecked(true);
|
||||||
|
stack->setCurrentIndex(0);
|
||||||
|
|
||||||
|
v->addWidget(header);
|
||||||
|
v->addWidget(stack, 1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 面板自绘表头(对齐原型):
|
||||||
|
// - buildPanelHeader:图标 + 标题 + (可选数量徽标) + 右侧操作按钮(标准面板)。
|
||||||
|
// - buildTabbedPanel:表头为若干 Tab(图标+标题+可选徽标)+ 右侧操作按钮,下方堆叠对应内容。
|
||||||
|
// 由调用方放在面板内容顶部,并隐藏 ADS 自带标题栏,从而完全掌控表头外观。
|
||||||
|
// 操作按钮当前为静态占位(tooltip 标注),后续接真实功能。
|
||||||
|
|
||||||
|
#include <QPair>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 一个表头操作项:图标 + 提示文案。
|
||||||
|
using HeaderAction = QPair<Glyph, QString>;
|
||||||
|
|
||||||
|
// 标准面板表头。标题 QLabel objectName="panelTitle"、徽标 QLabel objectName="panelBadge"
|
||||||
|
// (默认隐藏),调用方可经 container->findChild 动态更新。
|
||||||
|
QWidget* buildPanelHeader(Glyph icon, const QString& title,
|
||||||
|
const QVector<HeaderAction>& actions = {});
|
||||||
|
|
||||||
|
// 一个 Tab 规格:图标 + 标题 + 内容 + 是否带数量徽标。
|
||||||
|
struct PanelTab {
|
||||||
|
Glyph icon;
|
||||||
|
QString title;
|
||||||
|
QWidget* content;
|
||||||
|
bool hasBadge;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 带 Tab 表头的面板构建结果:容器 + 各 Tab 的徽标标签(无徽标处为 nullptr,供动态更新)。
|
||||||
|
struct TabbedPanel {
|
||||||
|
QWidget* container;
|
||||||
|
QVector<QLabel*> badges;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构建带 Tab 表头的面板(首个 Tab 默认激活;点击 Tab 切换下方堆叠内容)。
|
||||||
|
TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs,
|
||||||
|
const QVector<HeaderAction>& actions = {});
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,428 @@
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QStyleFactory>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。
|
||||||
|
// 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写,
|
||||||
|
// 就需要自带勾选 image,否则勾选态会变成空白方块。这里交给 Fusion 原生绘制,
|
||||||
|
// 它会自动采用调色板的 Highlight(#2D6CB5) 作勾选色,省去打包图片资源。
|
||||||
|
const char* kStyleSheet = R"QSS(
|
||||||
|
/* ── 基础 ───────────────────────────────────────────────── */
|
||||||
|
QWidget {
|
||||||
|
color: #1F2A3D;
|
||||||
|
}
|
||||||
|
QMainWindow, QDialog {
|
||||||
|
background: #F4F6FA;
|
||||||
|
}
|
||||||
|
QToolTip {
|
||||||
|
background: #1F2A3D;
|
||||||
|
color: #F4F6FA;
|
||||||
|
border: 1px solid #2D6CB5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 视图内工具条(2D/3D、数据详情):白底分段控件,柔和不刺眼 ── */
|
||||||
|
QToolBar {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #EAEEF4;
|
||||||
|
padding: 6px 8px;
|
||||||
|
spacing: 4px;
|
||||||
|
}
|
||||||
|
QToolBar QToolButton {
|
||||||
|
background: transparent;
|
||||||
|
color: #5A6B85;
|
||||||
|
border: none;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
QToolBar QToolButton:hover {
|
||||||
|
background: #EEF3FB;
|
||||||
|
color: #1F2A3D;
|
||||||
|
}
|
||||||
|
QToolBar QToolButton:pressed {
|
||||||
|
background: #DCE9F8;
|
||||||
|
}
|
||||||
|
QToolBar QToolButton:checked {
|
||||||
|
background: #EAF1FB;
|
||||||
|
color: #2D6CB5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QToolBar QToolButton:checked:hover {
|
||||||
|
background: #DCE9F8;
|
||||||
|
}
|
||||||
|
QToolBar::separator {
|
||||||
|
background: #EAEEF4;
|
||||||
|
width: 1px;
|
||||||
|
margin: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 树 / 列表:无边框(靠面板与留白分隔,去掉线框感)+ 充足行距 ── */
|
||||||
|
QTreeWidget, QListWidget, QTreeView, QListView {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
padding: 6px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
|
||||||
|
padding: 7px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 1px 4px;
|
||||||
|
}
|
||||||
|
QTreeWidget::item:hover, QListWidget::item:hover,
|
||||||
|
QTreeView::item:hover, QListView::item:hover {
|
||||||
|
background: #EEF3FB;
|
||||||
|
}
|
||||||
|
QTreeWidget::item:selected, QListWidget::item:selected,
|
||||||
|
QTreeView::item:selected, QListView::item:selected {
|
||||||
|
background: #DCE9F8;
|
||||||
|
color: #1B3D67;
|
||||||
|
}
|
||||||
|
/* 注意:不要给 QTreeView::branch 设 background——一旦改写 branch,Qt 会停止绘制
|
||||||
|
默认的展开/折叠箭头(与 indicator 同类陷阱),父节点折叠图标会消失。 */
|
||||||
|
|
||||||
|
/* 表头(对象显示栏) */
|
||||||
|
QHeaderView::section {
|
||||||
|
background: #EDF1F7;
|
||||||
|
color: #3A475C;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #D5DBE5;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 标签页(数据 / 文件):现代下划线 tab,无边框盒子 ──────── */
|
||||||
|
QTabWidget::pane {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #EAEEF4;
|
||||||
|
top: 0;
|
||||||
|
background: #FFFFFF;
|
||||||
|
}
|
||||||
|
QTabBar {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
QTabBar::tab {
|
||||||
|
background: transparent;
|
||||||
|
color: #5A6B85;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
QTabBar::tab:selected {
|
||||||
|
color: #2D6CB5;
|
||||||
|
border-bottom: 2px solid #2D6CB5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QTabBar::tab:hover:!selected {
|
||||||
|
color: #1F2A3D;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 复选框(仅调间距/字色,indicator 交给 Fusion 原生)──── */
|
||||||
|
QCheckBox {
|
||||||
|
spacing: 7px;
|
||||||
|
color: #1F2A3D;
|
||||||
|
}
|
||||||
|
QCheckBox:disabled {
|
||||||
|
color: #9AA6B6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 通用按钮 / 输入(登录窗内部各自再覆盖)────────────────── */
|
||||||
|
QPushButton {
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #1F2A3D;
|
||||||
|
border: 1px solid #C2CCDA;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: #EEF3FB;
|
||||||
|
border-color: #2D6CB5;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: #DCE9F8;
|
||||||
|
}
|
||||||
|
QPushButton:default {
|
||||||
|
background: #2D6CB5;
|
||||||
|
color: #FFFFFF;
|
||||||
|
border-color: #2D6CB5;
|
||||||
|
}
|
||||||
|
QPushButton:default:hover {
|
||||||
|
background: #2862A6;
|
||||||
|
}
|
||||||
|
QPushButton:disabled {
|
||||||
|
background: #F0F2F6;
|
||||||
|
color: #9AA6B6;
|
||||||
|
border-color: #DCE0E7;
|
||||||
|
}
|
||||||
|
QLineEdit {
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #1F2A3D;
|
||||||
|
border: 1px solid #C7D2E0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
selection-background-color: #2D6CB5;
|
||||||
|
selection-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
QLineEdit:focus {
|
||||||
|
border: 1px solid #2D6CB5;
|
||||||
|
}
|
||||||
|
QLineEdit:disabled {
|
||||||
|
background: #F0F2F6;
|
||||||
|
color: #8A93A3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background: transparent;
|
||||||
|
width: 12px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background: #C2CCDA;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:vertical:hover {
|
||||||
|
background: #A7B4C7;
|
||||||
|
}
|
||||||
|
QScrollBar:horizontal {
|
||||||
|
background: transparent;
|
||||||
|
height: 12px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:horizontal {
|
||||||
|
background: #C2CCDA;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:horizontal:hover {
|
||||||
|
background: #A7B4C7;
|
||||||
|
}
|
||||||
|
QScrollBar::add-line, QScrollBar::sub-line {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
QScrollBar::add-page, QScrollBar::sub-page {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 分隔条:默认近乎隐形,悬停时才显淡色(去掉灰硬条)──────── */
|
||||||
|
QSplitter::handle {
|
||||||
|
background: #EAEEF4;
|
||||||
|
}
|
||||||
|
QSplitter::handle:hover {
|
||||||
|
background: #C7D2E0;
|
||||||
|
}
|
||||||
|
ads--CDockSplitter::handle {
|
||||||
|
background: #EAEEF4;
|
||||||
|
}
|
||||||
|
ads--CDockSplitter::handle:hover {
|
||||||
|
background: #C7D2E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 状态栏:底部信息条(坐标系 / 状态指示,常驻可见)──────── */
|
||||||
|
QStatusBar {
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #5A6B85;
|
||||||
|
border-top: 1px solid #EAEEF4;
|
||||||
|
}
|
||||||
|
QStatusBar::item {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
QStatusBar QLabel {
|
||||||
|
color: #5A6B85;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 菜单栏 / 菜单(按需出现时也与主题一致)────────────────── */
|
||||||
|
QMenuBar {
|
||||||
|
background: #EDF1F7;
|
||||||
|
color: #1F2A3D;
|
||||||
|
border-bottom: 1px solid #D5DBE5;
|
||||||
|
}
|
||||||
|
QMenuBar::item {
|
||||||
|
background: transparent;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
QMenuBar::item:selected {
|
||||||
|
background: #DCE6F4;
|
||||||
|
}
|
||||||
|
QMenu {
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #1F2A3D;
|
||||||
|
border: 1px solid #D5DBE5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
QMenu::item {
|
||||||
|
padding: 6px 24px 6px 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
QMenu::item:selected {
|
||||||
|
background: #DCE9F8;
|
||||||
|
color: #1B3D67;
|
||||||
|
}
|
||||||
|
QMenu::separator {
|
||||||
|
height: 1px;
|
||||||
|
background: #E1E6EE;
|
||||||
|
margin: 5px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
|
||||||
|
QComboBox {
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #1F2A3D;
|
||||||
|
border: 1px solid #C2CCDA;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
QComboBox:hover {
|
||||||
|
border-color: #2D6CB5;
|
||||||
|
}
|
||||||
|
QComboBox:focus {
|
||||||
|
border-color: #2D6CB5;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down {
|
||||||
|
border: none;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid #D5DBE5;
|
||||||
|
border-radius: 6px;
|
||||||
|
selection-background-color: #DCE9F8;
|
||||||
|
selection-color: #1B3D67;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 分组框(按需出现时也与主题一致)──────────────────────── */
|
||||||
|
QGroupBox {
|
||||||
|
border: 1px solid #D5DBE5;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
left: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
color: #3A475C;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 进度条(长任务反馈,遵循 Doherty 阈值)──────────────────── */
|
||||||
|
QProgressBar {
|
||||||
|
background: #E6EBF3;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: #5A6B85;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background: #2D6CB5;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
|
||||||
|
面板已锁定(无关闭/浮动/拖动),每个区只一个标签即表头:统一浅色头 +
|
||||||
|
蓝色下划线强调 + 深色加粗标题。各区一致,读起来像固定子窗口表头。 */
|
||||||
|
ads--CDockAreaWidget {
|
||||||
|
background: #F4F6FA;
|
||||||
|
}
|
||||||
|
ads--CDockAreaTitleBar {
|
||||||
|
background: #EDF1F7;
|
||||||
|
border-bottom: 1px solid #D5DBE5;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ads--CDockWidgetTab {
|
||||||
|
background: #EDF1F7;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 7px 12px;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
ads--CDockWidgetTab[activeTab="true"] {
|
||||||
|
background: #EDF1F7;
|
||||||
|
border-bottom: 2px solid #2D6CB5;
|
||||||
|
}
|
||||||
|
ads--CDockWidgetTab QLabel {
|
||||||
|
color: #5A6B85;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
ads--CDockWidgetTab[activeTab="true"] QLabel {
|
||||||
|
color: #1F2A3D;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
)QSS";
|
||||||
|
|
||||||
|
// 浅色专业调色板:让标准控件在无 QSS 覆盖处也保持一致底色/选中色。
|
||||||
|
QPalette buildPalette()
|
||||||
|
{
|
||||||
|
QPalette p;
|
||||||
|
const QColor shell("#F4F6FA");
|
||||||
|
const QColor panel("#FFFFFF");
|
||||||
|
const QColor text("#1F2A3D");
|
||||||
|
const QColor mutedText("#5A6B85");
|
||||||
|
const QColor accent("#2D6CB5");
|
||||||
|
|
||||||
|
p.setColor(QPalette::Window, shell);
|
||||||
|
p.setColor(QPalette::WindowText, text);
|
||||||
|
p.setColor(QPalette::Base, panel);
|
||||||
|
p.setColor(QPalette::AlternateBase, QColor("#F0F3F8"));
|
||||||
|
p.setColor(QPalette::Text, text);
|
||||||
|
p.setColor(QPalette::Button, QColor("#EDF1F7"));
|
||||||
|
p.setColor(QPalette::ButtonText, text);
|
||||||
|
p.setColor(QPalette::ToolTipBase, QColor("#1F2A3D"));
|
||||||
|
p.setColor(QPalette::ToolTipText, shell);
|
||||||
|
p.setColor(QPalette::Highlight, accent);
|
||||||
|
p.setColor(QPalette::HighlightedText, panel);
|
||||||
|
p.setColor(QPalette::PlaceholderText, mutedText);
|
||||||
|
p.setColor(QPalette::Link, accent);
|
||||||
|
|
||||||
|
// 关键:把 Fusion 用于绘制 3D 凹凸(斜角/凹槽/分隔条阴影)的明暗角色统一压成相近浅灰,
|
||||||
|
// 立体效果即塌成平面。ADS 分隔条用 palette(dark),这样也变成一条扁平浅灰细线(无 3D)。
|
||||||
|
p.setColor(QPalette::Light, QColor("#FFFFFF"));
|
||||||
|
p.setColor(QPalette::Midlight, QColor("#EEF1F5"));
|
||||||
|
p.setColor(QPalette::Mid, QColor("#E1E6EE"));
|
||||||
|
p.setColor(QPalette::Dark, QColor("#D7DEE8"));
|
||||||
|
p.setColor(QPalette::Shadow, QColor("#D7DEE8"));
|
||||||
|
|
||||||
|
// 禁用态:统一灰化,避免 Fusion 默认禁用色偏暗看不清。
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::Text, QColor("#9AA6B6"));
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#9AA6B6"));
|
||||||
|
p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#9AA6B6"));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void applyTheme(QApplication& app)
|
||||||
|
{
|
||||||
|
// Fusion:跨平台一致且对 QSS 友好(Windows 原生风对部分控件会忽略样式表)。
|
||||||
|
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
||||||
|
|
||||||
|
// 基础字体:中文界面用 微软雅黑 UI 渲染最清爽;缺失时 Qt 自动回退。
|
||||||
|
// 10pt(≈13px)对齐主流商用客户端基准;9pt 偏小显拥挤。抗锯齿优先,观感更精致。
|
||||||
|
QFont base(QStringLiteral("Microsoft YaHei UI"), 10);
|
||||||
|
base.setStyleStrategy(QFont::PreferAntialias);
|
||||||
|
app.setFont(base);
|
||||||
|
|
||||||
|
app.setPalette(buildPalette());
|
||||||
|
app.setStyleSheet(QString::fromUtf8(kStyleSheet));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 全局视觉主题(浅色专业方向):Fusion 风格 + 浅色 QPalette + 结构化 QSS。
|
||||||
|
// 仅外观——不改任何信号槽 / 渲染 / 数据逻辑。在 QApplication 构造后、弹登录窗前调用一次。
|
||||||
|
//
|
||||||
|
// 设计令牌(与登录窗、视图详情浮层共享,保证全项目一脉相承):
|
||||||
|
// 外壳底 #F4F6FA 面板白 #FFFFFF 抬升/表头 #EDF1F7
|
||||||
|
// 强调 #2D6CB5 悬停 #2862A6 按下 #234F87 选中行 #DCE9F8
|
||||||
|
// 文字 #1F2A3D 次要文字 #5A6B85 边框 #D5DBE5 强边框 #C2CCDA
|
||||||
|
// 危险 #C0392B
|
||||||
|
|
||||||
|
class QApplication;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 应用浅色专业主题(Fusion + 调色板 + 全局样式表)。幂等,启动调用一次即可。
|
||||||
|
void applyTheme(QApplication& app);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
#include "TopBar.hpp"
|
||||||
|
|
||||||
|
#include <QActionGroup>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMenuBar>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ── 专业图标尺寸(统一放大;菜单栏字号亦同步加大)──
|
||||||
|
constexpr int kToolIcon = 22; // 工具条右侧图标
|
||||||
|
constexpr int kWorkspaceIcon = 20; // 工作空间 / 项目图标
|
||||||
|
|
||||||
|
// 竖直分隔细线。
|
||||||
|
QFrame* makeDivider(QWidget* parent)
|
||||||
|
{
|
||||||
|
auto* line = new QFrame(parent);
|
||||||
|
line->setObjectName(QStringLiteral("topDivider"));
|
||||||
|
line->setFrameShape(QFrame::VLine);
|
||||||
|
line->setFixedWidth(1);
|
||||||
|
line->setFixedHeight(24);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧图标按钮(仅图标,悬停显示文本)。
|
||||||
|
QToolButton* makeIconButton(QWidget* parent, Glyph g, const QString& tip)
|
||||||
|
{
|
||||||
|
auto* btn = new QToolButton(parent);
|
||||||
|
btn->setObjectName(QStringLiteral("iconBtn"));
|
||||||
|
btn->setIcon(makeGlyph(g, QColor("#5A6B85"), kToolIcon));
|
||||||
|
btn->setIconSize(QSize(kToolIcon, kToolIcon));
|
||||||
|
btn->setToolTip(tip);
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
|
||||||
|
QMenu* buildViewMenu(QWidget* p)
|
||||||
|
{
|
||||||
|
auto* m = new QMenu(QStringLiteral("视图"), p);
|
||||||
|
m->addAction(QStringLiteral("分析视图"));
|
||||||
|
m->addAction(QStringLiteral("大屏视图"));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu* buildProjectMenu(QWidget* p)
|
||||||
|
{
|
||||||
|
auto* m = new QMenu(QStringLiteral("项目管理"), p);
|
||||||
|
m->addAction(QStringLiteral("数据视图"));
|
||||||
|
auto* cfg = m->addMenu(QStringLiteral("项目配置"));
|
||||||
|
cfg->addAction(QStringLiteral("基本信息"));
|
||||||
|
cfg->addAction(QStringLiteral("项目结构"));
|
||||||
|
cfg->addAction(QStringLiteral("视图配置"));
|
||||||
|
m->addAction(QStringLiteral("数据管理"));
|
||||||
|
auto* biz = m->addMenu(QStringLiteral("业务管理"));
|
||||||
|
biz->addAction(QStringLiteral("异常管理"));
|
||||||
|
biz->addAction(QStringLiteral("异常体管理"));
|
||||||
|
auto* mon = m->addMenu(QStringLiteral("在线监测"));
|
||||||
|
mon->addAction(QStringLiteral("项目设备"));
|
||||||
|
mon->addAction(QStringLiteral("在线任务管理"));
|
||||||
|
auto* doc = m->addMenu(QStringLiteral("项目资料管理"));
|
||||||
|
doc->addAction(QStringLiteral("项目资料管理"));
|
||||||
|
doc->addAction(QStringLiteral("报告列表"));
|
||||||
|
auto* tools = m->addMenu(QStringLiteral("工具组件"));
|
||||||
|
tools->addAction(QStringLiteral("装置与脚本"));
|
||||||
|
tools->addAction(QStringLiteral("色阶配置"));
|
||||||
|
tools->addAction(QStringLiteral("异常类型管理"));
|
||||||
|
tools->addAction(QStringLiteral("模型管理"));
|
||||||
|
auto* exp = m->addMenu(QStringLiteral("批量导出"));
|
||||||
|
exp->addAction(QStringLiteral("文件导出"));
|
||||||
|
exp->addAction(QStringLiteral("报告导出"));
|
||||||
|
auto* alarm = m->addMenu(QStringLiteral("告警管理"));
|
||||||
|
alarm->addAction(QStringLiteral("设备告警"));
|
||||||
|
alarm->addAction(QStringLiteral("告警查询"));
|
||||||
|
m->addAction(QStringLiteral("自动任务"));
|
||||||
|
m->addAction(QStringLiteral("模板管理"));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu* buildToolsMenu(QWidget* p)
|
||||||
|
{
|
||||||
|
auto* m = new QMenu(QStringLiteral("业务工具"), p);
|
||||||
|
m->addAction(QStringLiteral("ERT 思维分析"));
|
||||||
|
m->addAction(QStringLiteral("电法脚本与装置"));
|
||||||
|
m->addAction(QStringLiteral("Geo 反演"));
|
||||||
|
m->addAction(QStringLiteral("三维 GPR 综合分析"));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu* buildDeviceMenu(QWidget* p)
|
||||||
|
{
|
||||||
|
auto* m = new QMenu(QStringLiteral("设备"), p);
|
||||||
|
m->addAction(QStringLiteral("连接设备"));
|
||||||
|
m->addAction(QStringLiteral("设备管理"));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QWidget* buildMenuBar(QWidget* parent)
|
||||||
|
{
|
||||||
|
auto* mb = new QMenuBar(parent);
|
||||||
|
mb->setObjectName(QStringLiteral("appMenuBar"));
|
||||||
|
// 自带样式(覆盖全局),加大字号/内边距,专业观感。
|
||||||
|
mb->setStyleSheet(QStringLiteral(
|
||||||
|
"#appMenuBar { background:#FFFFFF; border-bottom:1px solid #EEF1F5; padding:2px 8px; }"
|
||||||
|
"#appMenuBar::item { padding:7px 14px; border-radius:6px; font-size:14px; color:#1F2A3D; }"
|
||||||
|
"#appMenuBar::item:selected { background:#EAF1FB; color:#2D6CB5; }"
|
||||||
|
"#appMenuBar::item:pressed { background:#DCE6F4; }"));
|
||||||
|
mb->addMenu(buildViewMenu(mb));
|
||||||
|
mb->addMenu(buildProjectMenu(mb));
|
||||||
|
mb->addMenu(buildToolsMenu(mb));
|
||||||
|
mb->addMenu(buildDeviceMenu(mb));
|
||||||
|
return mb;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* buildTopToolBar(QWidget* parent)
|
||||||
|
{
|
||||||
|
auto* bar = new QWidget(parent);
|
||||||
|
bar->setObjectName(QStringLiteral("appToolBar"));
|
||||||
|
bar->setFixedHeight(56);
|
||||||
|
bar->setStyleSheet(QStringLiteral(
|
||||||
|
"#appToolBar { background:#FFFFFF; border-bottom:1px solid #E1E6EE; }"
|
||||||
|
"#topDivider { color:#E1E6EE; }"
|
||||||
|
"#wsSwitcher { color:#1F2A3D; border:none; border-radius:8px; padding:8px 12px;"
|
||||||
|
" font-size:14px; font-weight:600; }"
|
||||||
|
"#wsSwitcher:hover { background:#EEF3FB; }"
|
||||||
|
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
|
||||||
|
"QToolButton#iconBtn:hover { background:#EEF3FB; }"
|
||||||
|
"QToolButton::menu-indicator { image:none; }"
|
||||||
|
"#avatar { background:#2D6CB5; color:#FFFFFF; border-radius:17px; font-weight:700;"
|
||||||
|
" font-size:13px; }"
|
||||||
|
"#userName { color:#1F2A3D; font-size:13px; font-weight:600; }"
|
||||||
|
"#userRole { color:#8A93A3; font-size:11px; }"));
|
||||||
|
|
||||||
|
auto* lay = new QHBoxLayout(bar);
|
||||||
|
lay->setContentsMargins(14, 0, 14, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
|
||||||
|
// ── 工作空间切换(最左):显示当前空间,点击下拉切换 ──
|
||||||
|
auto* wsBtn = new QToolButton(bar);
|
||||||
|
wsBtn->setObjectName(QStringLiteral("wsSwitcher"));
|
||||||
|
wsBtn->setIcon(makeGlyph(Glyph::Workspace, QColor("#2D6CB5"), kWorkspaceIcon));
|
||||||
|
wsBtn->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
|
||||||
|
wsBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
|
wsBtn->setPopupMode(QToolButton::InstantPopup);
|
||||||
|
wsBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
|
||||||
|
auto* wsMenu = new QMenu(bar);
|
||||||
|
auto* wsHeader = wsMenu->addAction(QStringLiteral("切换空间"));
|
||||||
|
wsHeader->setEnabled(false);
|
||||||
|
wsMenu->addSeparator();
|
||||||
|
auto* wsGroup = new QActionGroup(bar);
|
||||||
|
wsGroup->setExclusive(true);
|
||||||
|
const QStringList spaces = {QStringLiteral("个人工作空间"), QStringLiteral("勘探一队"),
|
||||||
|
QStringLiteral("研究院共享")};
|
||||||
|
for (const auto& s : spaces) {
|
||||||
|
auto* a = wsMenu->addAction(s);
|
||||||
|
a->setCheckable(true);
|
||||||
|
wsGroup->addAction(a);
|
||||||
|
if (s == spaces.front()) a->setChecked(true);
|
||||||
|
QObject::connect(a, &QAction::triggered, wsBtn,
|
||||||
|
[wsBtn, s]() { wsBtn->setText(s + QStringLiteral(" ▾")); });
|
||||||
|
}
|
||||||
|
wsBtn->setMenu(wsMenu);
|
||||||
|
wsBtn->setText(spaces.front() + QStringLiteral(" ▾"));
|
||||||
|
lay->addWidget(wsBtn);
|
||||||
|
|
||||||
|
lay->addSpacing(10);
|
||||||
|
lay->addWidget(makeDivider(bar));
|
||||||
|
lay->addSpacing(10);
|
||||||
|
|
||||||
|
// ── 项目选择器(与工作空间切换同款样式:无边框 + 图标 + 文本 + 下拉)──
|
||||||
|
auto* projBtn = new QToolButton(bar);
|
||||||
|
projBtn->setObjectName(QStringLiteral("wsSwitcher"));
|
||||||
|
projBtn->setIcon(makeGlyph(Glyph::Folder, QColor("#2D6CB5"), kWorkspaceIcon));
|
||||||
|
projBtn->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
|
||||||
|
projBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
|
projBtn->setPopupMode(QToolButton::InstantPopup);
|
||||||
|
projBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
auto* projMenu = new QMenu(bar);
|
||||||
|
auto* projHeader = projMenu->addAction(QStringLiteral("切换项目"));
|
||||||
|
projHeader->setEnabled(false);
|
||||||
|
projMenu->addSeparator();
|
||||||
|
auto* projCur = projMenu->addAction(QStringLiteral("青海湖北岸勘探项目"));
|
||||||
|
projCur->setCheckable(true);
|
||||||
|
projCur->setChecked(true);
|
||||||
|
projBtn->setMenu(projMenu);
|
||||||
|
projBtn->setText(QStringLiteral("青海湖北岸勘探项目 青海·海北州 ▾"));
|
||||||
|
lay->addWidget(projBtn);
|
||||||
|
|
||||||
|
lay->addStretch();
|
||||||
|
|
||||||
|
// ── 右侧:帮助 / 通知 / 设置(仅图标,悬停显示文本)──
|
||||||
|
lay->addWidget(makeIconButton(bar, Glyph::Help, QStringLiteral("帮助")));
|
||||||
|
lay->addWidget(makeIconButton(bar, Glyph::Bell, QStringLiteral("通知")));
|
||||||
|
lay->addWidget(makeIconButton(bar, Glyph::Gear, QStringLiteral("设置")));
|
||||||
|
lay->addSpacing(10);
|
||||||
|
lay->addWidget(makeDivider(bar));
|
||||||
|
lay->addSpacing(12);
|
||||||
|
|
||||||
|
// ── 用户:圆形头像 + 姓名/职务 ──
|
||||||
|
auto* avatar = new QLabel(QStringLiteral("ZL"), bar);
|
||||||
|
avatar->setObjectName(QStringLiteral("avatar"));
|
||||||
|
avatar->setFixedSize(34, 34);
|
||||||
|
avatar->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(avatar);
|
||||||
|
lay->addSpacing(8);
|
||||||
|
|
||||||
|
auto* userBox = new QWidget(bar);
|
||||||
|
auto* userLay = new QVBoxLayout(userBox);
|
||||||
|
userLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
userLay->setSpacing(0);
|
||||||
|
auto* userName = new QLabel(QStringLiteral("张磊"), userBox);
|
||||||
|
userName->setObjectName(QStringLiteral("userName"));
|
||||||
|
auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBox);
|
||||||
|
userRole->setObjectName(QStringLiteral("userRole"));
|
||||||
|
userLay->addWidget(userName);
|
||||||
|
userLay->addWidget(userRole);
|
||||||
|
lay->addWidget(userBox);
|
||||||
|
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 顶部应用区(对齐原型,静态视觉壳):
|
||||||
|
// - buildMenuBar:最上方的菜单栏(视图 / 项目管理 / 业务工具 / 设备,含多级子菜单)。
|
||||||
|
// - buildTopToolBar:菜单栏下方的工具条(工作空间切换 + 项目选择 + 帮助/通知/设置 + 用户)。
|
||||||
|
// 调用方将两者纵向堆叠后经 QMainWindow::setMenuWidget 挂到主窗口顶部。
|
||||||
|
// 菜单/按钮当前为静态占位,后续接真实页面与数据。
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 顶部菜单栏(返回 QWidget*,内部是 QMenuBar;调用方放在最上一行)。
|
||||||
|
QWidget* buildMenuBar(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// 菜单栏下方的工具条(工作空间/项目/帮助/通知/设置/用户)。
|
||||||
|
QWidget* buildTopToolBar(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
|
@ -12,6 +11,7 @@
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
#include "AuthService.hpp"
|
#include "AuthService.hpp"
|
||||||
|
|
||||||
|
|
@ -77,76 +77,126 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
: QDialog(parent), auth_(auth)
|
: QDialog(parent), auth_(auth)
|
||||||
{
|
{
|
||||||
setWindowTitle(QStringLiteral("Geopro 3.0 登录"));
|
setWindowTitle(QStringLiteral("Geopro 3.0 登录"));
|
||||||
setFixedSize(380, 320);
|
setFixedSize(400, 500);
|
||||||
// 显式样式:QLineEdit 在所有状态都白底深字+边框(否则失焦时文字色取调色板默认、与背景相近不可见)。
|
|
||||||
|
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
|
||||||
|
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
|
||||||
setStyleSheet(QStringLiteral(
|
setStyleSheet(QStringLiteral(
|
||||||
"QDialog { background: #F5F7FD; }"
|
"QDialog { background: #F4F6FA; }"
|
||||||
"QLabel { color: #2B3A55; }"
|
"#headerBand {"
|
||||||
|
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
|
||||||
|
" stop:0 #2D6CB5, stop:1 #234F87); }"
|
||||||
|
"#brandTitle { color: #FFFFFF; font-size: 23px; font-weight: 700; }"
|
||||||
|
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: 12px; }"
|
||||||
|
"#fieldLabel { color: #5A6B85; font-size: 12px; font-weight: 600; }"
|
||||||
"QLineEdit {"
|
"QLineEdit {"
|
||||||
" background: #FFFFFF; color: #1F2A3D;"
|
" background: #FFFFFF; color: #1F2A3D;"
|
||||||
" border: 1px solid #C7D2E0; border-radius: 5px; padding: 6px 9px;"
|
" border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;"
|
||||||
" selection-background-color: #3A6EA5; selection-color: #FFFFFF; }"
|
" selection-background-color: #2D6CB5; selection-color: #FFFFFF; }"
|
||||||
"QLineEdit:focus { border: 1px solid #3A6EA5; }"
|
"QLineEdit:focus { border: 1px solid #2D6CB5; }"
|
||||||
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"));
|
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"
|
||||||
|
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }"));
|
||||||
|
|
||||||
auto* root = new QVBoxLayout(this);
|
auto* root = new QVBoxLayout(this);
|
||||||
root->setContentsMargins(30, 24, 30, 24);
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
root->setSpacing(14);
|
root->setSpacing(0);
|
||||||
|
|
||||||
auto* title = new QLabel(QStringLiteral("Geopro 3.0 登录"), this);
|
// ── 品牌头部:强调色横幅 + 产品名 + 副标题(建立产品身份与视觉锚点)──
|
||||||
QFont titleFont = title->font();
|
auto* headerBand = new QWidget(this);
|
||||||
titleFont.setPointSize(15);
|
headerBand->setObjectName(QStringLiteral("headerBand"));
|
||||||
titleFont.setBold(true);
|
headerBand->setFixedHeight(108);
|
||||||
title->setFont(titleFont);
|
auto* headerLayout = new QVBoxLayout(headerBand);
|
||||||
title->setAlignment(Qt::AlignCenter);
|
headerLayout->setContentsMargins(32, 0, 32, 0);
|
||||||
title->setStyleSheet(QStringLiteral("color: #2B3A55;"));
|
headerLayout->setSpacing(4);
|
||||||
root->addWidget(title);
|
headerLayout->addStretch();
|
||||||
|
auto* brandTitle = new QLabel(QStringLiteral("Geopro 3.0"), headerBand);
|
||||||
|
brandTitle->setObjectName(QStringLiteral("brandTitle"));
|
||||||
|
auto* brandSubtitle = new QLabel(QStringLiteral("地球物理数据分析平台"), headerBand);
|
||||||
|
brandSubtitle->setObjectName(QStringLiteral("brandSubtitle"));
|
||||||
|
headerLayout->addWidget(brandTitle);
|
||||||
|
headerLayout->addWidget(brandSubtitle);
|
||||||
|
headerLayout->addStretch();
|
||||||
|
root->addWidget(headerBand);
|
||||||
|
|
||||||
auto* form = new QFormLayout();
|
// ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)──
|
||||||
form->setSpacing(10);
|
auto* body = new QWidget(this);
|
||||||
form->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
auto* form = new QVBoxLayout(body);
|
||||||
|
form->setContentsMargins(32, 24, 32, 26);
|
||||||
|
form->setSpacing(6);
|
||||||
|
|
||||||
userEdit_ = new QLineEdit(QStringLiteral("sydk"), this);
|
// 统一字段构造:小号muted标签 + 40px 高输入框 + 字段间距。
|
||||||
pwdEdit_ = new QLineEdit(QStringLiteral("123456"), this);
|
auto addField = [&](const QString& labelText, QLineEdit* edit) {
|
||||||
|
auto* lbl = new QLabel(labelText, body);
|
||||||
|
lbl->setObjectName(QStringLiteral("fieldLabel"));
|
||||||
|
edit->setMinimumHeight(40);
|
||||||
|
form->addWidget(lbl);
|
||||||
|
form->addWidget(edit);
|
||||||
|
form->addSpacing(6);
|
||||||
|
};
|
||||||
|
|
||||||
|
userEdit_ = new QLineEdit(body);
|
||||||
|
userEdit_->setPlaceholderText(QStringLiteral("请输入用户名"));
|
||||||
|
userEdit_->setClearButtonEnabled(true);
|
||||||
|
addField(QStringLiteral("用户名"), userEdit_);
|
||||||
|
|
||||||
|
pwdEdit_ = new QLineEdit(body);
|
||||||
pwdEdit_->setEchoMode(QLineEdit::Password);
|
pwdEdit_->setEchoMode(QLineEdit::Password);
|
||||||
form->addRow(QStringLiteral("用户名"), userEdit_);
|
pwdEdit_->setPlaceholderText(QStringLiteral("请输入密码"));
|
||||||
form->addRow(QStringLiteral("密码"), pwdEdit_);
|
addField(QStringLiteral("密码"), pwdEdit_);
|
||||||
|
|
||||||
|
// 验证码:标签 + 一行(输入框占主,验证码图固定在右)。
|
||||||
|
auto* codeLbl = new QLabel(QStringLiteral("验证码"), body);
|
||||||
|
codeLbl->setObjectName(QStringLiteral("fieldLabel"));
|
||||||
|
form->addWidget(codeLbl);
|
||||||
|
|
||||||
// 验证码行:图 + 输入框 + 刷新
|
|
||||||
auto* captchaRow = new QHBoxLayout();
|
auto* captchaRow = new QHBoxLayout();
|
||||||
captchaLabel_ = new QLabel(this);
|
captchaRow->setSpacing(10);
|
||||||
|
codeEdit_ = new QLineEdit(body);
|
||||||
|
codeEdit_->setMinimumHeight(40);
|
||||||
|
codeEdit_->setPlaceholderText(QStringLiteral("请输入验证码"));
|
||||||
|
captchaLabel_ = new QLabel(body);
|
||||||
|
captchaLabel_->setObjectName(QStringLiteral("captchaImg"));
|
||||||
captchaLabel_->setFixedSize(kCaptchaWidth, kCaptchaHeight);
|
captchaLabel_->setFixedSize(kCaptchaWidth, kCaptchaHeight);
|
||||||
captchaLabel_->setFrameShape(QFrame::StyledPanel);
|
captchaLabel_->setAlignment(Qt::AlignCenter);
|
||||||
codeEdit_ = new QLineEdit(this);
|
|
||||||
codeEdit_->setPlaceholderText(QStringLiteral("验证码"));
|
|
||||||
captchaRow->addWidget(captchaLabel_);
|
|
||||||
captchaRow->addWidget(codeEdit_, 1);
|
captchaRow->addWidget(codeEdit_, 1);
|
||||||
form->addRow(QStringLiteral("验证码"), captchaRow);
|
captchaRow->addWidget(captchaLabel_);
|
||||||
|
form->addLayout(captchaRow);
|
||||||
|
|
||||||
refreshBtn_ = new QPushButton(QStringLiteral("看不清?刷新"), this);
|
// 刷新链接(右对齐,次要操作弱化为文字链接)。
|
||||||
|
auto* refreshRow = new QHBoxLayout();
|
||||||
|
refreshRow->addStretch();
|
||||||
|
refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body);
|
||||||
refreshBtn_->setFlat(true);
|
refreshBtn_->setFlat(true);
|
||||||
refreshBtn_->setCursor(Qt::PointingHandCursor);
|
refreshBtn_->setCursor(Qt::PointingHandCursor);
|
||||||
refreshBtn_->setStyleSheet(QStringLiteral("color: #3A6EA5; border: none; text-align: right;"));
|
refreshBtn_->setStyleSheet(QStringLiteral(
|
||||||
form->addRow(QString(), refreshBtn_);
|
"QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }"
|
||||||
|
"QPushButton:hover { color: #234F87; text-decoration: underline; }"));
|
||||||
|
refreshRow->addWidget(refreshBtn_);
|
||||||
|
form->addLayout(refreshRow);
|
||||||
|
|
||||||
root->addLayout(form);
|
// 错误提示:固定占位高度,避免出现时整体布局跳动。
|
||||||
|
errorLabel_ = new QLabel(body);
|
||||||
errorLabel_ = new QLabel(this);
|
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
|
||||||
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B;"));
|
|
||||||
errorLabel_->setWordWrap(true);
|
errorLabel_->setWordWrap(true);
|
||||||
errorLabel_->setMinimumHeight(16);
|
errorLabel_->setMinimumHeight(18);
|
||||||
root->addWidget(errorLabel_);
|
form->addWidget(errorLabel_);
|
||||||
|
|
||||||
loginBtn_ = new QPushButton(QStringLiteral("立即登录"), this);
|
form->addStretch();
|
||||||
loginBtn_->setMinimumHeight(34);
|
|
||||||
|
// 主操作:满宽强调主按钮(von Restorff:唯一高强调元素引导主流程)。
|
||||||
|
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body);
|
||||||
|
loginBtn_->setMinimumHeight(44);
|
||||||
loginBtn_->setCursor(Qt::PointingHandCursor);
|
loginBtn_->setCursor(Qt::PointingHandCursor);
|
||||||
loginBtn_->setStyleSheet(QStringLiteral(
|
loginBtn_->setStyleSheet(QStringLiteral(
|
||||||
"QPushButton { background: #3A6EA5; color: white; border: none; border-radius: 4px; "
|
"QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; "
|
||||||
"font-weight: bold; }"
|
"font-size: 15px; font-weight: 600; }"
|
||||||
"QPushButton:hover { background: #325E8C; }"
|
"QPushButton:hover { background: #2862A6; }"
|
||||||
|
"QPushButton:pressed { background: #234F87; }"
|
||||||
"QPushButton:disabled { background: #9FB4CC; }"));
|
"QPushButton:disabled { background: #9FB4CC; }"));
|
||||||
loginBtn_->setDefault(true);
|
loginBtn_->setDefault(true);
|
||||||
root->addWidget(loginBtn_);
|
form->addWidget(loginBtn_);
|
||||||
|
|
||||||
|
root->addWidget(body, 1);
|
||||||
|
|
||||||
connect(refreshBtn_, &QPushButton::clicked, this, &LoginWindow::refreshCaptcha);
|
connect(refreshBtn_, &QPushButton::clicked, this, &LoginWindow::refreshCaptcha);
|
||||||
connect(loginBtn_, &QPushButton::clicked, this, &LoginWindow::attemptLogin);
|
connect(loginBtn_, &QPushButton::clicked, this, &LoginWindow::attemptLogin);
|
||||||
|
|
@ -154,6 +204,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
|
connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin);
|
||||||
|
|
||||||
refreshCaptcha(); // 打开即拉一张验证码
|
refreshCaptcha(); // 打开即拉一张验证码
|
||||||
|
userEdit_->setFocus(); // 焦点落在第一个待填字段
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoginWindow::refreshCaptcha()
|
void LoginWindow::refreshCaptcha()
|
||||||
|
|
|
||||||
545
src/app/main.cpp
545
src/app/main.cpp
|
|
@ -1,11 +1,12 @@
|
||||||
// M1 工作台(视图重构 Task B):正确产品模型。
|
// M1 工作台(视图重构 Task B):正确产品模型。
|
||||||
// - 左 对象树:GS→TM→DS(复选框)。勾选 dd_section → 在中央当前视图显示该数据集,可多条共存。
|
// - 左上 对象显示栏:GS→TM(测线,复选框)。勾选测线 → 在中央显示其 dd_section,可多条共存。
|
||||||
|
// - 左下 数据真实显示栏:单击测线 → 列其采集批次(数据集,tab 数据/文件)。单击采集批次 → 数据详情+异常+属性。
|
||||||
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
|
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
|
||||||
// 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。
|
// 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。
|
||||||
// 三维视图 = 对每个勾选数据集 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。
|
// 三维视图 = 勾选测线的 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。
|
||||||
// 切视图 / 勾选变化 → 按当前勾选集重建对应内容。
|
// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)
|
||||||
// 注:dd_voxel 体素引擎(render::buildVoxelFromScatters)已就绪并验证,但**不**作为工具条开关
|
// / 切片(dd_slice,vtkImagePlaneWidget 在体素 image 上交互拖切面) / 地形(DEM 高程面 + 影像纹理)。
|
||||||
// (那样与二维/三维平级会令人困惑、且不在原型);待做 3D 图层控制(对齐原型「视图详情」浮层)再接。
|
// 切视图 / 勾选变化 / 图层变化 → 重建对应内容。
|
||||||
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
|
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
|
||||||
// 单击某 DS → 显示该数据集:
|
// 单击某 DS → 显示该数据集:
|
||||||
// 网格数据 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
|
// 网格数据 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame(全项目共享,保证多视图配准)。
|
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame(全项目共享,保证多视图配准)。
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <initializer_list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -23,12 +25,19 @@
|
||||||
|
|
||||||
#include <QActionGroup>
|
#include <QActionGroup>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QColor>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFrame>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTabWidget>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
#include <QStatusBar>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
|
|
@ -36,6 +45,8 @@
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <DockAreaTitleBar.h>
|
||||||
|
#include <DockAreaWidget.h>
|
||||||
#include <DockManager.h>
|
#include <DockManager.h>
|
||||||
#include <DockWidget.h>
|
#include <DockWidget.h>
|
||||||
|
|
||||||
|
|
@ -45,38 +56,51 @@
|
||||||
|
|
||||||
#include "ApiClient.hpp"
|
#include "ApiClient.hpp"
|
||||||
#include "AuthService.hpp"
|
#include "AuthService.hpp"
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
#include "PanelHeader.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
#include "TopBar.hpp"
|
||||||
#include "login/LoginWindow.hpp"
|
#include "login/LoginWindow.hpp"
|
||||||
#include "panels/AnomalyListPanel.hpp"
|
#include "panels/AnomalyListPanel.hpp"
|
||||||
|
#include "panels/DatasetListPanel.hpp"
|
||||||
|
|
||||||
#include "CameraPreset.hpp"
|
#include "CameraPreset.hpp"
|
||||||
|
#include "ColorLutBuilder.hpp"
|
||||||
#include "Scene.hpp"
|
#include "Scene.hpp"
|
||||||
|
#include "VoxelFromScatters.hpp"
|
||||||
#include "actors/AnomalyActor.hpp"
|
#include "actors/AnomalyActor.hpp"
|
||||||
#include "actors/CurtainActor.hpp"
|
#include "actors/CurtainActor.hpp"
|
||||||
|
#include "actors/ElectrodeActor.hpp"
|
||||||
#include "actors/GridContourActor.hpp"
|
#include "actors/GridContourActor.hpp"
|
||||||
#include "actors/MapLineActor.hpp"
|
#include "actors/MapLineActor.hpp"
|
||||||
#include "actors/ScatterActor.hpp"
|
#include "actors/ScatterActor.hpp"
|
||||||
|
#include "actors/TerrainActor.hpp"
|
||||||
|
|
||||||
|
#include "geo/CrsTransform.hpp"
|
||||||
#include "geo/GeoLocalFrame.hpp"
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <exception>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QVTKOpenGLStereoWidget.h>
|
#include <QVTKOpenGLStereoWidget.h>
|
||||||
#include <vtkGenericOpenGLRenderWindow.h>
|
#include <vtkGenericOpenGLRenderWindow.h>
|
||||||
|
#include <vtkImagePlaneWidget.h>
|
||||||
|
#include <vtkLookupTable.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
#include <vtkSmartPointer.h>
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// 角色:DS 项的 dd 类型存在 UserRole+1(dsId 存在 UserRole)。
|
// 角色:树 TM 项存 tmId(UserRole+2);数据列表 DS 项的 dsId/ddType 由 panels/DatasetListPanel 定义。
|
||||||
constexpr int kRoleDsId = Qt::UserRole;
|
constexpr int kRoleTmId = Qt::UserRole + 2;
|
||||||
constexpr int kRoleDdType = Qt::UserRole + 1;
|
|
||||||
|
|
||||||
// 从对象结构树构建 QTreeWidget:GS → TM → DS 三层。
|
// 从对象结构树构建 QTreeWidget:GS → TM 两层(对齐原型;DS=采集批次在左下「数据列表」,不进树)。
|
||||||
// DS 项可勾选(复选框):勾选驱动该测线竖直帘面在中央场景显示;UserRole 存 dsId、UserRole+1 存 ddType。
|
// TM(测线) 项可勾选(复选框):勾选驱动该测线的 dd_section 在中央场景显示;UserRole+2 存 tmId。
|
||||||
// 网格剖面(dd_section)默认勾选,启动即显示帘面。
|
// 含 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) {
|
||||||
|
|
@ -85,21 +109,27 @@ void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gs
|
||||||
for (const auto& tm : gs.tms) {
|
for (const auto& tm : gs.tms) {
|
||||||
auto* tmItem = new QTreeWidgetItem(gsItem);
|
auto* tmItem = new QTreeWidgetItem(gsItem);
|
||||||
tmItem->setText(0, QString::fromStdString(tm.name));
|
tmItem->setText(0, QString::fromStdString(tm.name));
|
||||||
for (const auto& ds : tm.dss) {
|
tmItem->setData(0, kRoleTmId, QString::fromStdString(tm.id));
|
||||||
auto* dsItem = new QTreeWidgetItem(tmItem);
|
tmItem->setFlags(tmItem->flags() | Qt::ItemIsUserCheckable);
|
||||||
dsItem->setText(0, QString::fromStdString(ds.name));
|
const bool hasSection =
|
||||||
dsItem->setData(0, kRoleDsId, QString::fromStdString(ds.id));
|
std::any_of(tm.dss.begin(), tm.dss.end(),
|
||||||
dsItem->setData(0, kRoleDdType, QString::fromStdString(ds.ddType));
|
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
|
||||||
dsItem->setFlags(dsItem->flags() | Qt::ItemIsUserCheckable);
|
tmItem->setCheckState(0, hasSection ? Qt::Checked : Qt::Unchecked);
|
||||||
// 网格剖面默认勾选 → 启动即显帘面;其余默认不勾。
|
|
||||||
dsItem->setCheckState(
|
|
||||||
0, ds.ddType == "dd_section" ? Qt::Checked : Qt::Unchecked);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tree->expandAll();
|
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 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
||||||
std::string readPem(const std::string& path)
|
std::string readPem(const std::string& path)
|
||||||
{
|
{
|
||||||
|
|
@ -132,6 +162,10 @@ constexpr float kScatterPointSize = 4.0F;
|
||||||
constexpr double kCurtainZScale = 3.0;
|
constexpr double kCurtainZScale = 3.0;
|
||||||
constexpr double kDetailYScale = 1.5;
|
constexpr double kDetailYScale = 1.5;
|
||||||
|
|
||||||
|
// 项目 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 工作台。
|
// 在给定 QMainWindow 上构建 M1 工作台。
|
||||||
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
|
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
|
||||||
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo)
|
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo)
|
||||||
|
|
@ -142,6 +176,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
const double lat0 = median(baseGrid.lat);
|
const double lat0 = median(baseGrid.lat);
|
||||||
const double lon0 = median(baseGrid.lon);
|
const double lon0 = median(baseGrid.lon);
|
||||||
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(lat0, lon0);
|
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(竖直帘面场景)─────────────────────────────────
|
// ── 中央 QVTK + Scene(竖直帘面场景)─────────────────────────────────
|
||||||
// Scene 非 QObject:堆分配,用 widget 销毁信号清理(widget 随 window 销毁)。
|
// Scene 非 QObject:堆分配,用 widget 销毁信号清理(widget 随 window 销毁)。
|
||||||
|
|
@ -158,9 +194,55 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。
|
// 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。
|
||||||
auto viewMode = std::make_shared<ViewMode>(ViewMode::Map2D);
|
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);
|
auto* dockManager = new ads::CDockManager(&window);
|
||||||
window.setCentralWidget(dockManager);
|
window.setCentralWidget(dockManager);
|
||||||
|
|
||||||
|
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
|
||||||
|
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。这里在其后追加同选择器规则覆盖为极淡分隔。
|
||||||
|
dockManager->setStyleSheet(
|
||||||
|
dockManager->styleSheet() +
|
||||||
|
QStringLiteral(
|
||||||
|
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: #EAEEF4; }"
|
||||||
|
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: #C7D2E0; }"));
|
||||||
|
|
||||||
|
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮),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 视图。
|
// 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。
|
||||||
auto* centerWidget = new QWidget();
|
auto* centerWidget = new QWidget();
|
||||||
auto* centerLayout = new QVBoxLayout(centerWidget);
|
auto* centerLayout = new QVBoxLayout(centerWidget);
|
||||||
|
|
@ -181,6 +263,41 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
centerLayout->addWidget(viewToolBar);
|
centerLayout->addWidget(viewToolBar);
|
||||||
centerLayout->addWidget(vtkWidget, 1);
|
centerLayout->addWidget(vtkWidget, 1);
|
||||||
|
|
||||||
|
// ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。
|
||||||
|
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。
|
||||||
|
auto* layerPanel = new QFrame(centerWidget);
|
||||||
|
layerPanel->setFrameShape(QFrame::StyledPanel);
|
||||||
|
layerPanel->setStyleSheet(
|
||||||
|
QStringLiteral("QFrame{background:rgba(255,255,255,0.96);border:1px solid #D5DBE5;"
|
||||||
|
"border-radius:8px;} QCheckBox{padding:2px 1px;color:#1F2A3D;}"
|
||||||
|
"QCheckBox:disabled{color:#9AA6B6;}"));
|
||||||
|
auto* layerLayout = new QVBoxLayout(layerPanel);
|
||||||
|
layerLayout->setContentsMargins(13, 10, 15, 11);
|
||||||
|
layerLayout->setSpacing(6);
|
||||||
|
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
|
||||||
|
layerTitle->setStyleSheet(QStringLiteral(
|
||||||
|
"font-weight:600;color:#2D6CB5;border:none;background:transparent;padding-bottom:3px;"));
|
||||||
|
auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)"));
|
||||||
|
chkCurtain->setChecked(true);
|
||||||
|
auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)"));
|
||||||
|
chkVoxel->setChecked(false);
|
||||||
|
auto* chkTerrain = new QCheckBox(QStringLiteral("地形(DEM+影像)"));
|
||||||
|
chkTerrain->setChecked(false);
|
||||||
|
auto* chkSlice = new QCheckBox(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);
|
||||||
|
}
|
||||||
|
layerLayout->addWidget(layerTitle);
|
||||||
|
layerLayout->addWidget(chkCurtain);
|
||||||
|
layerLayout->addWidget(chkVoxel);
|
||||||
|
layerLayout->addWidget(chkSlice);
|
||||||
|
layerLayout->addWidget(chkTerrain);
|
||||||
|
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
||||||
|
|
||||||
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
||||||
vtkDock->setWidget(centerWidget);
|
vtkDock->setWidget(centerWidget);
|
||||||
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
||||||
|
|
@ -217,68 +334,132 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto* actShowAnomaly = detailToolBar->addAction(QStringLiteral("显示异常"));
|
auto* actShowAnomaly = detailToolBar->addAction(QStringLiteral("显示异常"));
|
||||||
actShowAnomaly->setCheckable(true);
|
actShowAnomaly->setCheckable(true);
|
||||||
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
|
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
|
||||||
|
auto* actShowElectrodes = detailToolBar->addAction(QStringLiteral("显示电极"));
|
||||||
|
actShowElectrodes->setCheckable(true);
|
||||||
|
actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型)
|
||||||
|
auto* actShowContour = detailToolBar->addAction(QStringLiteral("显示等值线"));
|
||||||
|
actShowContour->setCheckable(true);
|
||||||
|
actShowContour->setChecked(true); // 默认显示等值线(对齐原型)
|
||||||
detailLayout->addWidget(detailToolBar);
|
detailLayout->addWidget(detailToolBar);
|
||||||
detailLayout->addWidget(detailWidget, 1);
|
detailLayout->addWidget(detailWidget, 1);
|
||||||
|
|
||||||
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
||||||
detailDock->setWidget(detailContainer);
|
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);
|
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
|
||||||
|
|
||||||
// 左 dock:对象树。
|
// 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。
|
||||||
auto* tree = new QTreeWidget();
|
auto structure = std::make_shared<std::vector<geopro::data::GsNode>>(repo.loadStructure());
|
||||||
tree->setHeaderLabel(QStringLiteral("对象"));
|
|
||||||
populateTree(tree, repo.loadStructure());
|
|
||||||
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象列表"));
|
|
||||||
leftDock->setWidget(tree);
|
|
||||||
dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
|
||||||
|
|
||||||
// 右上 dock:异常列表(对齐原型;颜色块 + 名称 + 位置/深/尺寸 + 勾选显隐,与数据详情异常联动)。
|
// 左上 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));
|
||||||
|
}
|
||||||
|
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
|
||||||
|
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"), tree,
|
||||||
|
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
|
||||||
|
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
||||||
|
|
||||||
|
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
||||||
|
auto* datasetTabs = new QTabWidget();
|
||||||
|
auto* datasetList = new QListWidget();
|
||||||
|
datasetList->setAlternatingRowColors(true);
|
||||||
|
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
||||||
|
auto* fileList = new QListWidget(); // M1 文件 tab 占位
|
||||||
|
{ // 空状态引导:M1 暂无文件来源,给出说明而非空白面板(识别优先于回忆)。
|
||||||
|
auto* hint = new QListWidgetItem(QStringLiteral("(M1 暂无关联文件)"), fileList);
|
||||||
|
hint->setFlags(Qt::NoItemFlags);
|
||||||
|
hint->setForeground(QColor("#9AA6B6"));
|
||||||
|
hint->setTextAlignment(Qt::AlignCenter);
|
||||||
|
}
|
||||||
|
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();
|
auto* anomalyList = new QListWidget();
|
||||||
anomalyList->setAlternatingRowColors(true);
|
anomalyList->setAlternatingRowColors(true);
|
||||||
auto* anomalyDock = new ads::CDockWidget(QStringLiteral("异常列表"));
|
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
|
||||||
anomalyDock->setWidget(anomalyList);
|
objAttrLabel->setWordWrap(true);
|
||||||
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, anomalyDock);
|
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||||
|
objAttrLabel->setMargin(8);
|
||||||
|
|
||||||
// 右下 dock:属性。
|
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 的数量徽标
|
||||||
|
|
||||||
|
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
|
||||||
|
rightDock->setWidget(anomalyPanel.container);
|
||||||
|
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
|
||||||
|
|
||||||
|
// 右下 dock:属性(数据集属性,键值;对齐原型下半,独立面板)。
|
||||||
auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)"));
|
auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)"));
|
||||||
propLabel->setWordWrap(true);
|
propLabel->setWordWrap(true);
|
||||||
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||||
propLabel->setMargin(8);
|
propLabel->setMargin(8);
|
||||||
auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
|
auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
|
||||||
propDock->setWidget(propLabel);
|
propDock->setWidget(
|
||||||
|
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel));
|
||||||
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
||||||
|
|
||||||
|
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
|
||||||
|
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
|
||||||
|
// 注:AlwaysShowTabs=true 时 ADS 不再自动改写标题栏可见性,手动隐藏可稳定保持。
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 中央视图重建(核心)─────────────────────────────────────────────
|
// ── 中央视图重建(核心)─────────────────────────────────────────────
|
||||||
// 两个互斥视图按当前勾选集整体重建:scene.clear() → 对每个勾选 dd_section 加对应 actor。
|
// 按勾选的测线(TM)整体重建:scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。
|
||||||
// 二维地图 = buildSurveyLine(红线俯视,浅底背景)+ applyTop2D。
|
// 二维地图 = buildSurveyLine(红线俯视,浅底背景)+ applyTop2D。
|
||||||
// 三维视图 = buildCurtain(断面墙)SetScale(1,1,kCurtainZScale) + applyFree3D(白底)。
|
// 三维视图 = buildCurtain(断面墙)SetScale(1,1,kCurtainZScale) + applyFree3D(白底)。
|
||||||
// frame 全局共享;切视图/勾选变化都调用此函数重建当前视图。
|
// frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。
|
||||||
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree]() {
|
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();
|
scene->clear();
|
||||||
|
|
||||||
const bool is2D = (*viewMode == ViewMode::Map2D);
|
const bool is2D = (*viewMode == ViewMode::Map2D);
|
||||||
rendererPtr->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0);
|
rendererPtr->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0);
|
||||||
|
|
||||||
// 遍历对象树收集所有勾选的 dd_section,逐个加入当前视图内容。
|
// 渲染单个 dd_section 数据集:二维=测线线;三维=帘面(受「帘面」图层开关控制)。
|
||||||
QList<QTreeWidgetItem*> stack;
|
auto renderSection = [&](const std::string& id) {
|
||||||
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 dsId = cur->data(0, kRoleDsId).toString();
|
|
||||||
if (dsId.isEmpty()) continue; // GS/TM 节点忽略
|
|
||||||
if (cur->checkState(0) != Qt::Checked) continue; // 仅显示勾选的
|
|
||||||
const QString ddType = cur->data(0, kRoleDdType).toString();
|
|
||||||
if (ddType != "dd_section") continue; // 当前仅支持剖面网格
|
|
||||||
|
|
||||||
const std::string id = dsId.toStdString();
|
|
||||||
const auto g = repo.loadGrid(id);
|
const auto g = repo.loadGrid(id);
|
||||||
if (is2D) {
|
if (is2D) {
|
||||||
auto line = geopro::render::buildSurveyLine(g, *frame);
|
auto line = geopro::render::buildSurveyLine(g, *frame);
|
||||||
if (line) scene->addActor(line);
|
if (line) scene->addActor(line);
|
||||||
} else {
|
} else if (*showCurtain) {
|
||||||
const auto cs = repo.loadColorScale(id);
|
const auto cs = repo.loadColorScale(id);
|
||||||
auto curtain = geopro::render::buildCurtain(g, cs, *frame);
|
auto curtain = geopro::render::buildCurtain(g, cs, *frame);
|
||||||
if (curtain) {
|
if (curtain) {
|
||||||
|
|
@ -286,6 +467,68 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
scene->addActor(curtain);
|
scene->addActor(curtain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 遍历对象树收集所有勾选的测线(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)
|
if (is2D)
|
||||||
|
|
@ -296,24 +539,40 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 勾选/取消某 dd_section → 重建当前视图内容(勾的才显示;可多条共存)。
|
// 勾选/取消某测线(TM) → 重建当前视图内容(勾的才显示;可多条共存)。
|
||||||
QObject::connect(tree, &QTreeWidget::itemChanged, tree,
|
QObject::connect(tree, &QTreeWidget::itemChanged, tree,
|
||||||
[rebuildCentral](QTreeWidgetItem* item, int) {
|
[rebuildCentral](QTreeWidgetItem* item, int) {
|
||||||
if (item->data(0, kRoleDsId).toString().isEmpty()) return; // GS/TM 忽略
|
if (item->data(0, kRoleTmId).toString().isEmpty()) return; // GS 忽略
|
||||||
rebuildCentral();
|
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(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
|
// 当前选中数据集 id(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
|
||||||
auto currentDsId = std::make_shared<QString>();
|
auto currentDsId = std::make_shared<QString>();
|
||||||
auto detailMode = std::make_shared<DetailMode>(DetailMode::Section18);
|
auto detailMode = std::make_shared<DetailMode>(DetailMode::Section18);
|
||||||
auto showAnomalies = std::make_shared<bool>(true); // 默认显示异常(对齐原型)
|
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 hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
|
||||||
|
|
||||||
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
|
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
|
||||||
// 勾选「显示异常」时在 #18/#17 上叠加异常 dashed 折线(同纵向夸张对齐)。
|
// 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。
|
||||||
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode,
|
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode,
|
||||||
showAnomalies, hiddenAnoms]() {
|
showAnomalies, showElectrodes, showContour, hiddenAnoms]() {
|
||||||
detailRendererPtr->RemoveAllViewProps();
|
detailRendererPtr->RemoveAllViewProps();
|
||||||
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
|
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
|
||||||
detailRenderWindowPtr->Render();
|
detailRenderWindowPtr->Render();
|
||||||
|
|
@ -321,7 +580,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
}
|
}
|
||||||
const std::string id = currentDsId->toStdString();
|
const std::string id = currentDsId->toStdString();
|
||||||
if (*detailMode == DetailMode::Section18) {
|
if (*detailMode == DetailMode::Section18) {
|
||||||
// 网格数据:#18 banded 等值面 + 等值线,两 actor 纵向夸张 1.5x(沿 y)。
|
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。
|
||||||
const auto g = repo.loadGrid(id);
|
const auto g = repo.loadGrid(id);
|
||||||
const auto cs = repo.loadColorScale(id);
|
const auto cs = repo.loadColorScale(id);
|
||||||
const auto actors = geopro::render::buildGridContour(g, cs);
|
const auto actors = geopro::render::buildGridContour(g, cs);
|
||||||
|
|
@ -329,10 +588,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
actors.bands->SetScale(1.0, kDetailYScale, 1.0);
|
actors.bands->SetScale(1.0, kDetailYScale, 1.0);
|
||||||
detailRendererPtr->AddViewProp(actors.bands);
|
detailRendererPtr->AddViewProp(actors.bands);
|
||||||
}
|
}
|
||||||
if (actors.edges) {
|
if (actors.edges && *showContour) {
|
||||||
actors.edges->SetScale(1.0, kDetailYScale, 1.0);
|
actors.edges->SetScale(1.0, kDetailYScale, 1.0);
|
||||||
detailRendererPtr->AddViewProp(actors.edges);
|
detailRendererPtr->AddViewProp(actors.edges);
|
||||||
}
|
}
|
||||||
|
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
|
||||||
|
if (*showElectrodes) {
|
||||||
|
auto elec = geopro::render::buildElectrodes(g);
|
||||||
|
if (elec) {
|
||||||
|
elec->SetScale(1.0, kDetailYScale, 1.0);
|
||||||
|
detailRendererPtr->AddViewProp(elec);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。
|
// 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。
|
||||||
const auto s = repo.loadScatter(id);
|
const auto s = repo.loadScatter(id);
|
||||||
|
|
@ -359,17 +626,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
detailRenderWindowPtr->Render();
|
detailRenderWindowPtr->Render();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 单击 DS → 记选中 + 重建数据详情 + 右侧属性(与勾选区分;不改帘面可见性)──
|
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
|
||||||
QObject::connect(
|
auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms,
|
||||||
tree, &QTreeWidget::itemClicked, tree,
|
anomalyBadge](const QString& dsId, const QString& name) {
|
||||||
[&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms](
|
if (dsId.isEmpty()) return;
|
||||||
QTreeWidgetItem* item, int) {
|
|
||||||
const QString dsId = item->data(0, kRoleDsId).toString();
|
|
||||||
if (dsId.isEmpty()) return; // GS/TM 节点无详情
|
|
||||||
const QString ddType = item->data(0, kRoleDdType).toString();
|
|
||||||
if (ddType != "dd_section") return;
|
|
||||||
const QString name = item->text(0);
|
|
||||||
|
|
||||||
*currentDsId = dsId;
|
*currentDsId = dsId;
|
||||||
|
|
||||||
// 右上异常列表:按该数据集异常重填(默认全显);先清隐藏集再填,避免重建时阻塞信号回灌。
|
// 右上异常列表:按该数据集异常重填(默认全显);先清隐藏集再填,避免重建时阻塞信号回灌。
|
||||||
|
|
@ -379,6 +639,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽
|
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽
|
||||||
geopro::app::populateAnomalyList(anomalyList, anomalies);
|
geopro::app::populateAnomalyList(anomalyList, anomalies);
|
||||||
}
|
}
|
||||||
|
// 异常列表 Tab 数量徽标。
|
||||||
|
if (anomalyBadge) {
|
||||||
|
anomalyBadge->setText(QString::number(anomalies.size()));
|
||||||
|
anomalyBadge->setVisible(!anomalies.empty());
|
||||||
|
}
|
||||||
|
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
|
|
||||||
|
|
@ -389,6 +654,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
"vmin / vmax: %4 / %5\n异常: %6 个")
|
"vmin / vmax: %4 / %5\n异常: %6 个")
|
||||||
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
||||||
.arg(anomalies.size()));
|
.arg(anomalies.size()));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 单击左下数据列表的采集批次(DS) → 加载到数据详情/异常/属性 ──
|
||||||
|
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; // 仅剖面网格有详情图
|
||||||
|
const QString name =
|
||||||
|
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
|
||||||
|
loadDataset(dsId, name);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
|
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
|
||||||
|
|
@ -414,36 +690,152 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ──「显示异常」开关:切换异常叠加 → 重建数据详情 ──
|
// ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ──
|
||||||
QObject::connect(actShowAnomaly, &QAction::toggled, detailWidget,
|
QObject::connect(actShowAnomaly, &QAction::toggled, detailWidget,
|
||||||
[showAnomalies, rebuildDetail](bool on) {
|
[showAnomalies, rebuildDetail](bool on) {
|
||||||
*showAnomalies = on;
|
*showAnomalies = on;
|
||||||
rebuildDetail();
|
rebuildDetail();
|
||||||
});
|
});
|
||||||
|
QObject::connect(actShowElectrodes, &QAction::toggled, detailWidget,
|
||||||
|
[showElectrodes, rebuildDetail](bool on) {
|
||||||
|
*showElectrodes = on;
|
||||||
|
rebuildDetail();
|
||||||
|
});
|
||||||
|
QObject::connect(actShowContour, &QAction::toggled, detailWidget,
|
||||||
|
[showContour, rebuildDetail](bool on) {
|
||||||
|
*showContour = on;
|
||||||
|
rebuildDetail();
|
||||||
|
});
|
||||||
|
|
||||||
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 按当前勾选集重建对应内容 ──
|
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。
|
||||||
QObject::connect(act2D, &QAction::triggered, vtkWidget, [viewMode, rebuildCentral]() {
|
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, &QAction::triggered, vtkWidget,
|
||||||
|
[viewMode, rebuildCentral, showLayerPanel]() {
|
||||||
*viewMode = ViewMode::Map2D;
|
*viewMode = ViewMode::Map2D;
|
||||||
|
showLayerPanel(false);
|
||||||
rebuildCentral();
|
rebuildCentral();
|
||||||
});
|
});
|
||||||
QObject::connect(act3D, &QAction::triggered, vtkWidget, [viewMode, rebuildCentral]() {
|
QObject::connect(act3D, &QAction::triggered, vtkWidget,
|
||||||
|
[viewMode, rebuildCentral, showLayerPanel]() {
|
||||||
*viewMode = ViewMode::View3D;
|
*viewMode = ViewMode::View3D;
|
||||||
|
showLayerPanel(true);
|
||||||
rebuildCentral();
|
rebuildCentral();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 启动默认:dd_section 已勾选,但 itemChanged 在 connect 之前触发故未渲染。
|
// ──「视图详情」图层勾选 → 更新图层显隐 → 重建中央 ──
|
||||||
// 这里 connect 之后主动按默认视图(二维地图)重建一次中央内容。
|
QObject::connect(chkCurtain, &QCheckBox::toggled, vtkWidget,
|
||||||
|
[showCurtain, rebuildCentral](bool on) {
|
||||||
|
*showCurtain = on;
|
||||||
rebuildCentral();
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 启动默认:测线已勾选,但 itemChanged 在 connect 之前触发故未渲染;这里重建一次中央内容。
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
|
||||||
|
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
|
||||||
|
{
|
||||||
|
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(geopro::app::buildTopToolBar(topChrome));
|
||||||
|
window.setMenuWidget(topChrome);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部状态栏:常驻显示坐标系与世界系原点(wayfinding:用户随时知道当前空间基准)。
|
||||||
|
window.statusBar()->showMessage(
|
||||||
|
QStringLiteral("就绪 | 坐标系 %1 | 世界系原点 %2, %3")
|
||||||
|
.arg(QString::fromUtf8(kProjectCrs))
|
||||||
|
.arg(lat0, 0, 'f', 5)
|
||||||
|
.arg(lon0, 0, 'f', 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
|
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
|
||||||
|
// 必须在 QApplication 构造前设置。
|
||||||
|
QApplication::setHighDpiScaleFactorRoundingPolicy(
|
||||||
|
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||||
|
|
||||||
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
|
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
|
||||||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
|
// 浅色专业主题(Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。
|
||||||
|
geopro::app::applyTheme(app);
|
||||||
|
|
||||||
|
// 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 读取)。
|
// 网络层:共享会话 ApiClient + 登录编排 AuthService(RSA 公钥从 resources 读取)。
|
||||||
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
|
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");
|
const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem");
|
||||||
|
|
@ -462,6 +854,7 @@ int main(int argc, char* argv[])
|
||||||
QMainWindow window;
|
QMainWindow window;
|
||||||
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
|
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
|
||||||
window.resize(1280, 800);
|
window.resize(1280, 800);
|
||||||
|
window.setMinimumSize(1024, 680); // 防止停靠面板被压到不可用尺寸
|
||||||
|
|
||||||
buildWorkbench(window, repo);
|
buildWorkbench(window, repo);
|
||||||
window.show();
|
window.show();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
#include "panels/DatasetListPanel.hpp"
|
||||||
|
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QListWidgetItem>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// dd 类型 → 中文标注。
|
||||||
|
QString ddTypeLabel(const std::string& ddType)
|
||||||
|
{
|
||||||
|
if (ddType == "dd_section") return QStringLiteral("剖面网格");
|
||||||
|
if (ddType == "dd_voxel") return QStringLiteral("体素");
|
||||||
|
return QString::fromStdString(ddType);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsNode>& dss)
|
||||||
|
{
|
||||||
|
if (!list) return;
|
||||||
|
list->clear();
|
||||||
|
for (const auto& ds : dss) {
|
||||||
|
const QString name = QString::fromStdString(ds.name);
|
||||||
|
const QString label = ddTypeLabel(ds.ddType);
|
||||||
|
QString text = name;
|
||||||
|
if (!label.isEmpty()) text += QStringLiteral("\n%1").arg(label);
|
||||||
|
|
||||||
|
auto* item = new QListWidgetItem(text, list);
|
||||||
|
item->setData(kDsIdRole, QString::fromStdString(ds.id));
|
||||||
|
item->setData(kDsDdTypeRole, QString::fromStdString(ds.ddType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "repo/RepoTypes.hpp"
|
||||||
|
|
||||||
|
class QListWidget;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 数据列表条目角色(与 main.cpp 树一致:Qt::UserRole=dsId、+1=ddType)。
|
||||||
|
constexpr int kDsIdRole = 0x0100; // Qt::UserRole
|
||||||
|
constexpr int kDsDdTypeRole = 0x0101; // Qt::UserRole + 1
|
||||||
|
|
||||||
|
// 用某测线(TM)的数据集(采集批次)填充 QListWidget(对齐原型左下「数据真实显示栏」)。
|
||||||
|
// 每条目 = 名称 +(ddType 标注);UserRole 存 dsId、+1 存 ddType(供单击驱动数据详情)。
|
||||||
|
// 清空旧条目后重填。
|
||||||
|
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsNode>& dss);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -25,6 +25,8 @@ constexpr const char* kScatterColorScaleFile = u8"剖面原数据的色阶数据
|
||||||
constexpr const char* kVoxelScatterFile1 = u8"剖面原数据1.txt";
|
constexpr const char* kVoxelScatterFile1 = u8"剖面原数据1.txt";
|
||||||
constexpr const char* kVoxelScatterFile2 = u8"剖面原数据2.txt";
|
constexpr const char* kVoxelScatterFile2 = u8"剖面原数据2.txt";
|
||||||
constexpr const char* kAnomalyFile = u8"剖面网格数据1——对应的异常圈定数据.txt";
|
constexpr const char* kAnomalyFile = u8"剖面网格数据1——对应的异常圈定数据.txt";
|
||||||
|
constexpr const char* kDemFile = u8"dem.tif";
|
||||||
|
constexpr const char* kImageFile = u8"image.tif";
|
||||||
|
|
||||||
// 校验 dsId,未知则抛错(输入边界验证)。
|
// 校验 dsId,未知则抛错(输入边界验证)。
|
||||||
void requireKnownDs(const std::string& dsId) {
|
void requireKnownDs(const std::string& dsId) {
|
||||||
|
|
@ -99,6 +101,9 @@ ColorScale LocalSampleRepository::loadScatterColorScale(const std::string& dsId)
|
||||||
return parseColorScale(readFile(kScatterColorScaleFile));
|
return parseColorScale(readFile(kScatterColorScaleFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string LocalSampleRepository::demPath() const { return dirUtf8_ + kDemFile; }
|
||||||
|
std::string LocalSampleRepository::imagePath() const { return dirUtf8_ + kImageFile; }
|
||||||
|
|
||||||
std::vector<ScatterField> LocalSampleRepository::loadVoxelScatters() {
|
std::vector<ScatterField> LocalSampleRepository::loadVoxelScatters() {
|
||||||
std::vector<ScatterField> out;
|
std::vector<ScatterField> out;
|
||||||
out.push_back(parseScatter(readFile(kVoxelScatterFile1)));
|
out.push_back(parseScatter(readFile(kVoxelScatterFile1)));
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ public:
|
||||||
// 具体类专有方法(不进 IDatasetRepository 接口);散点点为不透明,alpha 量纲差异无影响。
|
// 具体类专有方法(不进 IDatasetRepository 接口);散点点为不透明,alpha 量纲差异无影响。
|
||||||
geopro::core::ColorScale loadScatterColorScale(const std::string& dsId);
|
geopro::core::ColorScale loadScatterColorScale(const std::string& dsId);
|
||||||
|
|
||||||
|
// DEM/影像 GeoTIFF 的绝对路径(供 render::buildTerrain 经 GDAL 读)。
|
||||||
|
std::string demPath() const;
|
||||||
|
std::string imagePath() const;
|
||||||
|
|
||||||
// dd_voxel 输入:读两条交叉剖面散点(剖面原数据1.txt + 剖面原数据2.txt),
|
// dd_voxel 输入:读两条交叉剖面散点(剖面原数据1.txt + 剖面原数据2.txt),
|
||||||
// 返回两者,供体素插值合并。具体类专有方法(不进 IDatasetRepository 接口)。
|
// 返回两者,供体素插值合并。具体类专有方法(不进 IDatasetRepository 接口)。
|
||||||
std::vector<geopro::core::ScatterField> loadVoxelScatters();
|
std::vector<geopro::core::ScatterField> loadVoxelScatters();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets)
|
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets IOImage)
|
||||||
|
find_package(GDAL CONFIG REQUIRED)
|
||||||
add_library(geopro_render STATIC
|
add_library(geopro_render STATIC
|
||||||
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp)
|
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp)
|
||||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES})
|
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
||||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||||
set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
||||||
vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES})
|
vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES})
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>
|
||||||
double cellXY,
|
double cellXY,
|
||||||
double cellZ,
|
double cellZ,
|
||||||
double power,
|
double power,
|
||||||
double maxDist)
|
double maxDist,
|
||||||
|
double zDisplayScale)
|
||||||
{
|
{
|
||||||
// 1) 配准所有点到世界局部米 + 深度,组装 IDW 输入点集。
|
// 1) 配准所有点到世界局部米 + 深度,组装 IDW 输入点集。
|
||||||
geopro::core::PointSet pts;
|
geopro::core::PointSet pts;
|
||||||
|
|
@ -89,10 +90,11 @@ VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>
|
||||||
if (!(vmin < vmax)) { vmin = 0.0; vmax = 1.0; }
|
if (!(vmin < vmax)) { vmin = 0.0; vmax = 1.0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) 体绘制 + 暴露 image(供切片)。
|
// 5) 体绘制 + 暴露 image(供切片)。纵向夸张烤进 image 的 z 原点/间距(IDW 已用真实 cellZ 采样),
|
||||||
|
// 使体绘制/切片/帘面纵向一致(切片穿过体素而非在旁)。
|
||||||
VoxelResult out;
|
VoxelResult out;
|
||||||
out.volume = buildVoxel(vol, cs, spec.ox, spec.oy, spec.oz, spec.dx, spec.dy, spec.dz, vmin,
|
out.volume = buildVoxel(vol, cs, spec.ox, spec.oy, spec.oz * zDisplayScale, spec.dx, spec.dy,
|
||||||
vmax, out.image);
|
spec.dz * zDisplayScale, vmin, vmax, out.image);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ struct VoxelResult {
|
||||||
// 垂向 z = -ylist(深度向下,与帘面 z 取负一致)。
|
// 垂向 z = -ylist(深度向下,与帘面 z 取负一致)。
|
||||||
// IDW:maxDist 裁剪约束插值域(两交叉测线→十字片,设计 §10);NaN 留空→体绘制透明。
|
// IDW:maxDist 裁剪约束插值域(两交叉测线→十字片,设计 §10);NaN 留空→体绘制透明。
|
||||||
// crs 须为「项目 CRS(如 EPSG:4547) → EPSG:4326」;frame 与帘面/地图共用以保证空间配准。
|
// crs 须为「项目 CRS(如 EPSG:4547) → EPSG:4326」;frame 与帘面/地图共用以保证空间配准。
|
||||||
|
// zDisplayScale:把纵向夸张"烤进"输出 image 的 z 间距与原点(IDW 采样仍用真实 cellZ),
|
||||||
|
// 使体绘制 + 切片(vtkImagePlaneWidget 作用于此 image) + 帘面(actor SetScale 同倍)三者纵向一致。
|
||||||
// 输入不足(无点)返回 valid()==false 的空结果。
|
// 输入不足(无点)返回 valid()==false 的空结果。
|
||||||
VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>& profiles,
|
VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>& profiles,
|
||||||
const geopro::core::ColorScale& cs,
|
const geopro::core::ColorScale& cs,
|
||||||
|
|
@ -32,6 +34,7 @@ VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>
|
||||||
double cellXY = 1.0,
|
double cellXY = 1.0,
|
||||||
double cellZ = 0.5,
|
double cellZ = 0.5,
|
||||||
double power = 2.0,
|
double power = 2.0,
|
||||||
double maxDist = 4.0);
|
double maxDist = 4.0,
|
||||||
|
double zDisplayScale = 1.0);
|
||||||
|
|
||||||
} // namespace geopro::render
|
} // namespace geopro::render
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
#include "actors/ElectrodeActor.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <vtkCellArray.h>
|
||||||
|
#include <vtkNew.h>
|
||||||
|
#include <vtkPoints.h>
|
||||||
|
#include <vtkPolyData.h>
|
||||||
|
#include <vtkPolyDataMapper.h>
|
||||||
|
#include <vtkProperty.h>
|
||||||
|
#include <vtkTriangle.h>
|
||||||
|
|
||||||
|
namespace geopro::render {
|
||||||
|
|
||||||
|
vtkSmartPointer<vtkActor> buildElectrodes(const geopro::core::Grid& g, double markerW,
|
||||||
|
double markerH)
|
||||||
|
{
|
||||||
|
const int nx = g.nx();
|
||||||
|
if (nx < 1 || g.x.size() < static_cast<size_t>(nx) || g.y.empty()) {
|
||||||
|
return vtkSmartPointer<vtkActor>::New();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剖面顶边(最浅) y_screen = -min(深度);#18 用 -y 使深部在下,故最浅处 y_screen 最大。
|
||||||
|
const double minDepth = *std::min_element(g.y.begin(), g.y.end());
|
||||||
|
const double yTop = -minDepth;
|
||||||
|
|
||||||
|
vtkNew<vtkPoints> points;
|
||||||
|
vtkNew<vtkCellArray> tris;
|
||||||
|
// 每个电极一个朝下三角:顶点(apex)触剖面顶边 (x, yTop),底边在其上方 (x±w, yTop+h)。
|
||||||
|
for (int i = 0; i < nx; ++i) {
|
||||||
|
const double x = g.x[i];
|
||||||
|
const vtkIdType a = points->InsertNextPoint(x, yTop, 0.0); // apex(下)
|
||||||
|
const vtkIdType l = points->InsertNextPoint(x - markerW, yTop + markerH, 0.0);
|
||||||
|
const vtkIdType r = points->InsertNextPoint(x + markerW, yTop + markerH, 0.0);
|
||||||
|
vtkNew<vtkTriangle> t;
|
||||||
|
t->GetPointIds()->SetId(0, a);
|
||||||
|
t->GetPointIds()->SetId(1, l);
|
||||||
|
t->GetPointIds()->SetId(2, r);
|
||||||
|
tris->InsertNextCell(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
vtkNew<vtkPolyData> poly;
|
||||||
|
poly->SetPoints(points);
|
||||||
|
poly->SetPolys(tris);
|
||||||
|
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputData(poly);
|
||||||
|
mapper->ScalarVisibilityOff();
|
||||||
|
|
||||||
|
auto actor = vtkSmartPointer<vtkActor>::New();
|
||||||
|
actor->SetMapper(mapper);
|
||||||
|
actor->GetProperty()->SetColor(0.15, 0.15, 0.15); // 深灰 ▼
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vtkActor.h>
|
||||||
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
|
||||||
|
namespace geopro::render {
|
||||||
|
|
||||||
|
// 剖面顶部电极标记(对齐原型 ▼):在剖面顶边(最浅深度)各 x 列位置画一个朝下的小三角。
|
||||||
|
// 坐标与 #18 数据详情一致:x=g.x[i]、顶边 y=-min(深度)、z=0。调用方应施加与剖面相同的
|
||||||
|
// SetScale 以对齐(三角会随之略拉伸)。markerW/markerH 为数据单位下的三角半宽/高。
|
||||||
|
// 退化网格(无 x)返回空 actor。
|
||||||
|
vtkSmartPointer<vtkActor> buildElectrodes(const geopro::core::Grid& g,
|
||||||
|
double markerW = 0.5,
|
||||||
|
double markerH = 1.4);
|
||||||
|
|
||||||
|
} // namespace geopro::render
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
#include "actors/TerrainActor.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gdal.h>
|
||||||
|
#include <gdal_priv.h>
|
||||||
|
|
||||||
|
#include <vtkFloatArray.h>
|
||||||
|
#include <vtkImageData.h>
|
||||||
|
#include <vtkNew.h>
|
||||||
|
#include <vtkPointData.h>
|
||||||
|
#include <vtkPoints.h>
|
||||||
|
#include <vtkPolyData.h>
|
||||||
|
#include <vtkPolyDataMapper.h>
|
||||||
|
#include <vtkStructuredGrid.h>
|
||||||
|
#include <vtkStructuredGridGeometryFilter.h>
|
||||||
|
#include <vtkTexture.h>
|
||||||
|
#include <vtkUnsignedCharArray.h>
|
||||||
|
|
||||||
|
#include "geo/CrsTransform.hpp"
|
||||||
|
|
||||||
|
namespace geopro::render {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 项目 CRS(DEM 无投影信息时的兜底)。
|
||||||
|
constexpr const char* kFallbackCrs = "EPSG:4547";
|
||||||
|
|
||||||
|
struct Raster {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
double gt[6] = {0, 1, 0, 0, 0, 1}; // 仿射: x=gt0+col*gt1+row*gt2, y=gt3+col*gt4+row*gt5
|
||||||
|
std::string wkt;
|
||||||
|
bool ok() const { return w > 0 && h > 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用 GDAL 读影像像素为 RGB 纹理(vtkTIFFReader 对压缩/分块 TIFF 不可靠,故走 GDAL)。
|
||||||
|
// 行翻转使影像在 vtkImageData 中正立(配合 tcoord v=1-row/h)。读失败返回 nullptr。
|
||||||
|
vtkSmartPointer<vtkTexture> readTexture(const std::string& path) {
|
||||||
|
auto* ds = static_cast<GDALDataset*>(GDALOpen(path.c_str(), GA_ReadOnly));
|
||||||
|
if (!ds) return nullptr;
|
||||||
|
const int w = ds->GetRasterXSize(), h = ds->GetRasterYSize();
|
||||||
|
const int nb = std::min(ds->GetRasterCount(), 3);
|
||||||
|
if (w <= 0 || h <= 0 || nb < 1) { GDALClose(ds); return nullptr; }
|
||||||
|
|
||||||
|
std::vector<unsigned char> buf(static_cast<size_t>(w) * h * nb);
|
||||||
|
// 交错读 RGB:pixelSpace=nb, lineSpace=w*nb, bandSpace=1。
|
||||||
|
const CPLErr err =
|
||||||
|
ds->RasterIO(GF_Read, 0, 0, w, h, buf.data(), w, h, GDT_Byte, nb, nullptr,
|
||||||
|
nb, static_cast<GSpacing>(w) * nb, 1);
|
||||||
|
GDALClose(ds);
|
||||||
|
if (err != CE_None) return nullptr;
|
||||||
|
|
||||||
|
vtkNew<vtkImageData> img;
|
||||||
|
img->SetDimensions(w, h, 1);
|
||||||
|
vtkNew<vtkUnsignedCharArray> sc;
|
||||||
|
sc->SetNumberOfComponents(3);
|
||||||
|
sc->SetNumberOfTuples(static_cast<vtkIdType>(w) * h);
|
||||||
|
for (int r = 0; r < h; ++r)
|
||||||
|
for (int c = 0; c < w; ++c) {
|
||||||
|
const size_t s = (static_cast<size_t>(r) * w + c) * nb;
|
||||||
|
const vtkIdType d = static_cast<vtkIdType>(h - 1 - r) * w + c; // 行翻转→正立
|
||||||
|
unsigned char rgb[3];
|
||||||
|
rgb[0] = buf[s];
|
||||||
|
rgb[1] = nb > 1 ? buf[s + 1] : buf[s];
|
||||||
|
rgb[2] = nb > 2 ? buf[s + 2] : buf[s];
|
||||||
|
sc->SetTypedTuple(d, rgb);
|
||||||
|
}
|
||||||
|
img->GetPointData()->SetScalars(sc);
|
||||||
|
|
||||||
|
auto tex = vtkSmartPointer<vtkTexture>::New();
|
||||||
|
tex->SetInputData(img);
|
||||||
|
tex->InterpolateOn();
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读栅格几何信息(不读像素)。
|
||||||
|
Raster readGeo(GDALDataset* ds) {
|
||||||
|
Raster r;
|
||||||
|
if (!ds) return r;
|
||||||
|
r.w = ds->GetRasterXSize();
|
||||||
|
r.h = ds->GetRasterYSize();
|
||||||
|
ds->GetGeoTransform(r.gt);
|
||||||
|
const char* p = ds->GetProjectionRef();
|
||||||
|
if (p) r.wkt = p;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
vtkSmartPointer<vtkActor> buildTerrain(const std::string& demPath, const std::string& imagePath,
|
||||||
|
const geopro::core::GeoLocalFrame& frame, double zOffset,
|
||||||
|
double zScale)
|
||||||
|
{
|
||||||
|
GDALAllRegister();
|
||||||
|
|
||||||
|
auto* dem = static_cast<GDALDataset*>(GDALOpen(demPath.c_str(), GA_ReadOnly));
|
||||||
|
if (!dem) return vtkSmartPointer<vtkActor>::New();
|
||||||
|
const Raster dg = readGeo(dem);
|
||||||
|
if (!dg.ok()) { GDALClose(dem); return vtkSmartPointer<vtkActor>::New(); }
|
||||||
|
|
||||||
|
// DEM 高程像素(float)。
|
||||||
|
std::vector<float> elev(static_cast<size_t>(dg.w) * dg.h, 0.0F);
|
||||||
|
GDALRasterBand* band = dem->GetRasterBand(1);
|
||||||
|
int hasNoData = 0;
|
||||||
|
const double noData = band->GetNoDataValue(&hasNoData);
|
||||||
|
band->RasterIO(GF_Read, 0, 0, dg.w, dg.h, elev.data(), dg.w, dg.h, GDT_Float32, 0, 0);
|
||||||
|
GDALClose(dem);
|
||||||
|
|
||||||
|
// 有效高程范围(忽略 nodata),nodata 填为最小有效高程使其平坦。
|
||||||
|
float vmin = std::numeric_limits<float>::infinity();
|
||||||
|
float vmax = -std::numeric_limits<float>::infinity();
|
||||||
|
for (float v : elev)
|
||||||
|
if (!(hasNoData && v == static_cast<float>(noData))) {
|
||||||
|
vmin = std::min(vmin, v);
|
||||||
|
vmax = std::max(vmax, v);
|
||||||
|
}
|
||||||
|
if (!(vmin <= vmax)) { vmin = vmax = 0.0F; }
|
||||||
|
|
||||||
|
// 坐标变换:DEM CRS → 4326;4326 → 3857(纹理坐标用)。
|
||||||
|
const std::string demCrs = dg.wkt.empty() ? std::string(kFallbackCrs) : dg.wkt;
|
||||||
|
geopro::core::CrsTransform demTo4326(demCrs, "EPSG:4326");
|
||||||
|
geopro::core::CrsTransform llTo3857("EPSG:4326", "EPSG:3857");
|
||||||
|
|
||||||
|
// 影像几何(算纹理坐标);像素经 vtkTIFFReader 读为纹理。
|
||||||
|
Raster ig;
|
||||||
|
auto* img = static_cast<GDALDataset*>(GDALOpen(imagePath.c_str(), GA_ReadOnly));
|
||||||
|
if (img) { ig = readGeo(img); GDALClose(img); }
|
||||||
|
const bool hasImage = ig.ok();
|
||||||
|
|
||||||
|
// 结构化网格:点=世界局部米(E,N,elev*zScale),标量=高程;纹理坐标(若有影像)。
|
||||||
|
vtkNew<vtkStructuredGrid> sgrid;
|
||||||
|
sgrid->SetDimensions(dg.w, dg.h, 1);
|
||||||
|
vtkNew<vtkPoints> points;
|
||||||
|
points->SetNumberOfPoints(static_cast<vtkIdType>(dg.w) * dg.h);
|
||||||
|
vtkNew<vtkFloatArray> sc;
|
||||||
|
sc->SetName("elev");
|
||||||
|
sc->SetNumberOfTuples(static_cast<vtkIdType>(dg.w) * dg.h);
|
||||||
|
vtkNew<vtkFloatArray> tc;
|
||||||
|
tc->SetName("tc");
|
||||||
|
tc->SetNumberOfComponents(2);
|
||||||
|
if (hasImage) tc->SetNumberOfTuples(static_cast<vtkIdType>(dg.w) * dg.h);
|
||||||
|
|
||||||
|
for (int j = 0; j < dg.h; ++j)
|
||||||
|
for (int i = 0; i < dg.w; ++i) {
|
||||||
|
const double demX = dg.gt[0] + (i + 0.5) * dg.gt[1] + (j + 0.5) * dg.gt[2];
|
||||||
|
const double demY = dg.gt[3] + (i + 0.5) * dg.gt[4] + (j + 0.5) * dg.gt[5];
|
||||||
|
const auto ll = demTo4326.forward(demX, demY); // (lon, lat)
|
||||||
|
const auto local = frame.toLocal(ll.y, ll.x); // (E, N)
|
||||||
|
const vtkIdType id = static_cast<vtkIdType>(j) * dg.w + i;
|
||||||
|
float z = elev[static_cast<size_t>(j) * dg.w + i];
|
||||||
|
if (hasNoData && z == static_cast<float>(noData)) z = vmin;
|
||||||
|
points->SetPoint(id, local.x, local.y, (z - zOffset) * zScale); // rebase 到测线高程
|
||||||
|
sc->SetValue(id, z);
|
||||||
|
if (hasImage) {
|
||||||
|
const auto m = llTo3857.forward(ll.x, ll.y); // (mercX, mercY)
|
||||||
|
const double col = (m.x - ig.gt[0]) / ig.gt[1];
|
||||||
|
const double row = (m.y - ig.gt[3]) / ig.gt[5]; // gt5<0
|
||||||
|
const double u = col / ig.w;
|
||||||
|
const double v = 1.0 - row / ig.h; // 翻转使影像正立(按需核对)
|
||||||
|
tc->SetTuple2(id, u, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sgrid->SetPoints(points);
|
||||||
|
sgrid->GetPointData()->SetScalars(sc);
|
||||||
|
if (hasImage) sgrid->GetPointData()->SetTCoords(tc);
|
||||||
|
|
||||||
|
vtkNew<vtkStructuredGridGeometryFilter> geom;
|
||||||
|
geom->SetInputData(sgrid);
|
||||||
|
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputConnection(geom->GetOutputPort());
|
||||||
|
|
||||||
|
auto actor = vtkSmartPointer<vtkActor>::New();
|
||||||
|
actor->SetMapper(mapper);
|
||||||
|
|
||||||
|
auto tex = hasImage ? readTexture(imagePath) : nullptr;
|
||||||
|
if (tex) {
|
||||||
|
actor->SetTexture(tex);
|
||||||
|
mapper->ScalarVisibilityOff(); // 用纹理,不用高程标量上色
|
||||||
|
} else {
|
||||||
|
mapper->SetScalarRange(vmin, vmax); // 退化:影像读失败/缺失 → 按高程上色
|
||||||
|
}
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <vtkActor.h>
|
||||||
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
|
|
||||||
|
namespace geopro::render {
|
||||||
|
|
||||||
|
// DEM 地形 + 影像贴图(spec ④)。GDAL 读 dem.tif(高程栅格)与 image.tif(影像,EPSG:3857);
|
||||||
|
// 各自按其 CRS → EPSG:4326 → GeoLocalFrame 局部米配准到世界系(与帘面/体素同系);
|
||||||
|
// 高程作 z 起伏(vtkStructuredGrid 面),影像按经纬→3857→像素 算纹理坐标贴面。
|
||||||
|
// 影像加载失败则退化为按高程上色(无纹理)。读不到 DEM 返回空 actor。
|
||||||
|
//
|
||||||
|
// 依赖 GDAL/PROJ;调用方运行时须有 PROJ_DATA。
|
||||||
|
// zOffset:从高程减去的基准(米)——传测线地表高程使地形落在测线附近(否则按绝对高程浮空,
|
||||||
|
// spec M-3 Z 基准)。世界 z = (elev - zOffset) * zScale。zScale 为纵向夸张。
|
||||||
|
vtkSmartPointer<vtkActor> buildTerrain(const std::string& demPath,
|
||||||
|
const std::string& imagePath,
|
||||||
|
const geopro::core::GeoLocalFrame& frame,
|
||||||
|
double zOffset = 0.0,
|
||||||
|
double zScale = 1.0);
|
||||||
|
|
||||||
|
} // namespace geopro::render
|
||||||
|
|
@ -74,6 +74,9 @@ vtkSmartPointer<vtkVolume> buildVoxel(const geopro::core::ScalarVolume& vol,
|
||||||
// SmartVolumeMapper:有 GPU 走 GPU ray cast,否则自动回退 CPU,避免无 GPU 时卡死/失败。
|
// SmartVolumeMapper:有 GPU 走 GPU ray cast,否则自动回退 CPU,避免无 GPU 时卡死/失败。
|
||||||
vtkNew<vtkSmartVolumeMapper> mapper;
|
vtkNew<vtkSmartVolumeMapper> mapper;
|
||||||
mapper->SetInputData(img);
|
mapper->SetInputData(img);
|
||||||
|
// 全程统一全质量(GPU 足够快, 实测 ~7ms/帧):关掉交互降采样, 避免"停手补高清"那一帧突跳停顿。
|
||||||
|
mapper->SetAutoAdjustSampleDistances(0);
|
||||||
|
mapper->SetInteractiveAdjustSampleDistances(0);
|
||||||
|
|
||||||
vtkNew<vtkVolumeProperty> prop;
|
vtkNew<vtkVolumeProperty> prop;
|
||||||
prop->SetColor(color);
|
prop->SetColor(color);
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ target_sources(geopro_tests PRIVATE render/test_curtain.cpp)
|
||||||
target_sources(geopro_tests PRIVATE render/test_scatter.cpp)
|
target_sources(geopro_tests PRIVATE render/test_scatter.cpp)
|
||||||
# Anomaly:buildAnomalies(markType 点/线/面 -> vtkActor) 几何/闭合/颜色/y取负/空跳过。
|
# Anomaly:buildAnomalies(markType 点/线/面 -> vtkActor) 几何/闭合/颜色/y取负/空跳过。
|
||||||
target_sources(geopro_tests PRIVATE render/test_anomaly.cpp)
|
target_sources(geopro_tests PRIVATE render/test_anomaly.cpp)
|
||||||
|
# Electrode:buildElectrodes(剖面顶边朝下三角 ▼) 三角数/顶点位置/空安全。
|
||||||
|
target_sources(geopro_tests PRIVATE render/test_electrode.cpp)
|
||||||
|
# Terrain:buildTerrain(GDAL 读 dem/image + 重投影 → warp 面+纹理) 非空/缺文件安全(需 PROJ_DATA)。
|
||||||
|
target_sources(geopro_tests PRIVATE render/test_terrain.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <vtkPolyData.h>
|
||||||
|
#include <vtkPolyDataMapper.h>
|
||||||
|
|
||||||
|
#include "actors/ElectrodeActor.hpp"
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::core;
|
||||||
|
|
||||||
|
// buildElectrodes: nx 列 → nx 个朝下三角(各 3 点);顶点 y = -min(深度)(剖面顶边)。
|
||||||
|
TEST(Electrode, BuildsDownTrianglesAtSectionTop) {
|
||||||
|
Grid g(3, 2);
|
||||||
|
g.x = {0.0, 10.0, 20.0};
|
||||||
|
g.y = {2.0, 8.0}; // 深度;min=2 → 顶边 y_screen = -2
|
||||||
|
auto actor = geopro::render::buildElectrodes(g, /*markerW*/ 0.5, /*markerH*/ 1.0);
|
||||||
|
ASSERT_NE(actor.GetPointer(), nullptr);
|
||||||
|
|
||||||
|
auto* mapper = vtkPolyDataMapper::SafeDownCast(actor->GetMapper());
|
||||||
|
ASSERT_NE(mapper, nullptr);
|
||||||
|
auto* poly = vtkPolyData::SafeDownCast(mapper->GetInput());
|
||||||
|
ASSERT_NE(poly, nullptr);
|
||||||
|
EXPECT_EQ(poly->GetNumberOfCells(), 3); // 3 电极 → 3 三角
|
||||||
|
EXPECT_EQ(poly->GetNumberOfPoints(), 3 * 3); // 每三角 3 点
|
||||||
|
|
||||||
|
// 第一个三角的 apex(点 0) 在 (x=0, y=-2)(顶边),底边两点在其上方 y=-1。
|
||||||
|
double apex[3];
|
||||||
|
poly->GetPoint(0, apex);
|
||||||
|
EXPECT_DOUBLE_EQ(apex[0], 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(apex[1], -2.0);
|
||||||
|
double base[3];
|
||||||
|
poly->GetPoint(1, base);
|
||||||
|
EXPECT_DOUBLE_EQ(base[1], -1.0); // -2 + markerH(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退化网格(无 x)返回空 actor。
|
||||||
|
TEST(Electrode, EmptyGridYieldsSafeActor) {
|
||||||
|
Grid g(0, 0);
|
||||||
|
auto actor = geopro::render::buildElectrodes(g);
|
||||||
|
ASSERT_NE(actor.GetPointer(), nullptr);
|
||||||
|
EXPECT_EQ(actor->GetMapper(), nullptr);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <vtkActor.h>
|
||||||
|
#include <vtkMapper.h>
|
||||||
|
|
||||||
|
#include "actors/TerrainActor.hpp"
|
||||||
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::core;
|
||||||
|
|
||||||
|
// 样本 DEM/影像目录(UTF-8 中文路径;源文件以 UTF-8 保存 + MSVC /utf-8 编译)。
|
||||||
|
static const std::string kDir =
|
||||||
|
u8"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/";
|
||||||
|
|
||||||
|
// buildTerrain: 真实 dem.tif + image.tif 经 GDAL 读 + 重投影 → 非空 actor 且有 mapper。
|
||||||
|
// (需 PROJ_DATA + GDAL 运行时; tests CMake 注入 PROJ_DATA。)
|
||||||
|
TEST(Terrain, BuildsTexturedSurfaceFromSampleDemImage) {
|
||||||
|
GeoLocalFrame frame(22.546, 114.164); // 测区附近(香港)
|
||||||
|
auto actor = geopro::render::buildTerrain(kDir + "dem.tif", kDir + "image.tif", frame);
|
||||||
|
ASSERT_NE(actor.GetPointer(), nullptr);
|
||||||
|
ASSERT_NE(actor->GetMapper(), nullptr); // 成功读 DEM → 有 mapper(空 actor 无 mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不存在的 DEM → 安全返回空 actor(无 mapper),不崩。
|
||||||
|
TEST(Terrain, MissingDemYieldsSafeActor) {
|
||||||
|
GeoLocalFrame frame(22.546, 114.164);
|
||||||
|
auto actor = geopro::render::buildTerrain("D:/no/such/dem.tif", "D:/no/such/img.tif", frame);
|
||||||
|
ASSERT_NE(actor.GetPointer(), nullptr);
|
||||||
|
EXPECT_EQ(actor->GetMapper(), nullptr);
|
||||||
|
}
|
||||||
|
|
@ -10,9 +10,11 @@
|
||||||
|
|
||||||
#include <vtkActor.h>
|
#include <vtkActor.h>
|
||||||
#include <vtkNew.h>
|
#include <vtkNew.h>
|
||||||
|
#include <vtkOpenGLRenderWindow.h>
|
||||||
#include <vtkPNGWriter.h>
|
#include <vtkPNGWriter.h>
|
||||||
#include <vtkRenderWindow.h>
|
#include <vtkRenderWindow.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
|
#include <vtkSmartVolumeMapper.h>
|
||||||
#include <vtkWindowToImageFilter.h>
|
#include <vtkWindowToImageFilter.h>
|
||||||
|
|
||||||
#include "CameraPreset.hpp"
|
#include "CameraPreset.hpp"
|
||||||
|
|
@ -20,9 +22,11 @@
|
||||||
#include "VoxelFromScatters.hpp"
|
#include "VoxelFromScatters.hpp"
|
||||||
#include "actors/AnomalyActor.hpp"
|
#include "actors/AnomalyActor.hpp"
|
||||||
#include "actors/CurtainActor.hpp"
|
#include "actors/CurtainActor.hpp"
|
||||||
|
#include "actors/ElectrodeActor.hpp"
|
||||||
#include "actors/GridContourActor.hpp"
|
#include "actors/GridContourActor.hpp"
|
||||||
#include "actors/MapLineActor.hpp"
|
#include "actors/MapLineActor.hpp"
|
||||||
#include "actors/ScatterActor.hpp"
|
#include "actors/ScatterActor.hpp"
|
||||||
|
#include "actors/TerrainActor.hpp"
|
||||||
#include "geo/CrsTransform.hpp"
|
#include "geo/CrsTransform.hpp"
|
||||||
#include "geo/GeoLocalFrame.hpp"
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
#include "parse/SampleParsers.hpp"
|
#include "parse/SampleParsers.hpp"
|
||||||
|
|
@ -110,6 +114,9 @@ int main() {
|
||||||
act->SetScale(1.0, exag, 1.0);
|
act->SetScale(1.0, exag, 1.0);
|
||||||
ren->AddActor(act);
|
ren->AddActor(act);
|
||||||
}
|
}
|
||||||
|
// 顶部电极标记 ▼(同纵向夸张对齐)。
|
||||||
|
auto elec = render::buildElectrodes(g);
|
||||||
|
if (elec) { elec->SetScale(1.0, exag, 1.0); ren->AddActor(elec); }
|
||||||
render::applyTop2D(ren);
|
render::applyTop2D(ren);
|
||||||
renderToPng(ren, (dir + "verify_section_anomaly.png").c_str(), 1100, 360);
|
renderToPng(ren, (dir + "verify_section_anomaly.png").c_str(), 1100, 360);
|
||||||
std::printf("ANOMALY n=%zu\n", anomalies.size());
|
std::printf("ANOMALY n=%zu\n", anomalies.size());
|
||||||
|
|
@ -143,6 +150,28 @@ int main() {
|
||||||
if (vr.valid()) vr.image->GetDimensions(dims);
|
if (vr.valid()) vr.image->GetDimensions(dims);
|
||||||
std::printf("VOXEL valid=%d dims=%dx%dx%d pts=%zu\n", vr.valid() ? 1 : 0,
|
std::printf("VOXEL valid=%d dims=%dx%dx%d pts=%zu\n", vr.valid() ? 1 : 0,
|
||||||
dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size());
|
dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size());
|
||||||
|
|
||||||
|
// [实测诊断] 体绘制实际渲染模式 + OpenGL 渲染器(查是否软件渲染/CPU 回退)。
|
||||||
|
if (vr.valid()) {
|
||||||
|
vtkNew<vtkRenderer> rd; rd->AddVolume(vr.volume);
|
||||||
|
vtkNew<vtkRenderWindow> rwd; rwd->SetOffScreenRendering(1);
|
||||||
|
rwd->AddRenderer(rd); rwd->SetSize(400, 400); rwd->Render();
|
||||||
|
auto* svm = vtkSmartVolumeMapper::SafeDownCast(vr.volume->GetMapper());
|
||||||
|
const int mode = svm ? svm->GetLastUsedRenderMode() : -99;
|
||||||
|
std::printf("VOXEL_RENDER_MODE=%d (0=Default,1=RayCast/CPU,2=GPU,3=OSPRay)\n", mode);
|
||||||
|
auto* gl = vtkOpenGLRenderWindow::SafeDownCast(rwd.Get());
|
||||||
|
if (gl) std::printf("GL_CAPS:\n%s\n", gl->ReportCapabilities());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) DEM 地形 + 影像贴图 — GDAL 读 + 重投影到世界系 + warp 面 + 纹理
|
||||||
|
{
|
||||||
|
auto terr = render::buildTerrain(dir + "dem.tif", dir + "image.tif", frame, 0.0, 1.0);
|
||||||
|
vtkNew<vtkRenderer> ren; ren->SetBackground(0.50, 0.60, 0.72);
|
||||||
|
if (terr) ren->AddActor(terr);
|
||||||
|
render::applyFree3D(ren);
|
||||||
|
renderToPng(ren, (dir + "verify_terrain_3d.png").c_str(), 800, 600);
|
||||||
|
std::printf("TERRAIN actor=%d\n", terr ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0);
|
std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"description": "Geopro 3.0 desktop client (Qt6 + VTK9) - M1. 方案②-修订: Qt/VTK/ADS/QtKeychain 对接官方 MSVC Qt(不走 vcpkg); 仅非 Qt 依赖走 vcpkg, 按层递增。",
|
"description": "Geopro 3.0 desktop client (Qt6 + VTK9) - M1. 方案②-修订: Qt/VTK/ADS/QtKeychain 对接官方 MSVC Qt(不走 vcpkg); 仅非 Qt 依赖走 vcpkg, 按层递增。",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"eigen3",
|
"eigen3",
|
||||||
|
"gdal",
|
||||||
"gtest",
|
"gtest",
|
||||||
"nlohmann-json",
|
"nlohmann-json",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue