Merge pull request 'feat/vtk-merged-dataset-column' (#10) from feat/vtk-merged-dataset-column into main
Reviewed-on: https://gitea.geomative.cn/gaozheng/geopro/pulls/10
This commit is contained in:
commit
bbe53dbe86
|
|
@ -13,6 +13,8 @@ set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
|
# 显式钉死 MSVC 运行时库:Release=/MD,Debug=/MDd,杜绝静默漂移(ABI 一致性,见 ENV_SETUP §9.2)。
|
||||||
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
add_compile_options(/utf-8 /MP /W4 /permissive-)
|
add_compile_options(/utf-8 /MP /W4 /permissive-)
|
||||||
# 生成 PDB——即使 Release 优化构建也产出调试符号,使 minidump / 运行期崩溃栈可符号化分析
|
# 生成 PDB——即使 Release 优化构建也产出调试符号,使 minidump / 运行期崩溃栈可符号化分析
|
||||||
# (生产桌面端排障必需)。/Zi 编译期调试信息;/DEBUG 链接产 PDB;/OPT:REF,ICF 抵消 /DEBUG
|
# (生产桌面端排障必需)。/Zi 编译期调试信息;/DEBUG 链接产 PDB;/OPT:REF,ICF 抵消 /DEBUG
|
||||||
|
|
@ -31,16 +33,61 @@ endif()
|
||||||
# - 仅非 Qt 依赖(GDAL/PROJ/OpenSSL/Eigen/...)走 vcpkg
|
# - 仅非 Qt 依赖(GDAL/PROJ/OpenSSL/Eigen/...)走 vcpkg
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 构建环境护栏(把「配置错→运行时随机崩溃」变成「配置期一眼可查」)
|
||||||
|
# 校准基准:本机 Qt 6.11.1 msvc2022_64 + VTK 9.6.x + VS2026-preview 工具集。
|
||||||
|
# 工具集只 WARN 不 FATAL(本机 VS2026-preview 与预编译 Qt v143 ABI 兼容、可正常构建)。
|
||||||
|
# 详见 docs/ENV_SETUP_Windows.md §4/§5/§9。
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
# QT_ROOT 环境变量必须已设且指向有效的官方 MSVC Qt kit(预设的 CMAKE_PREFIX_PATH 读它)。
|
||||||
|
if(NOT DEFINED ENV{QT_ROOT} OR "$ENV{QT_ROOT}" STREQUAL ""
|
||||||
|
OR NOT EXISTS "$ENV{QT_ROOT}/lib/cmake/Qt6")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"QT_ROOT 未设置或无效。请 setx QT_ROOT \"<你的Qt>\\6.11.1\\msvc2022_64\" 后重开终端。见 docs/ENV_SETUP_Windows.md §4。")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)
|
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)
|
||||||
|
|
||||||
|
# Qt 版本护栏:major.minor 必须是 6.11(任意补丁号可)。换大小版本会与源码编的 VTK/ADS ABI/兼容性错位。
|
||||||
|
if(NOT Qt6_VERSION VERSION_LESS 6.11 AND Qt6_VERSION VERSION_LESS 6.12)
|
||||||
|
# 6.11.x —— 合规。
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"Qt 版本不匹配:找到 ${Qt6_VERSION},需要 6.11.x(与源码编的 VTK/ADS 兼容)。请安装 6.11.1 msvc2022_64 并把 QT_ROOT 指向它。见 docs/ENV_SETUP_Windows.md §4。")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# VTK 就位护栏(friendly):find_package REQUIRED 也会失败,但这里给出可操作的原因。
|
||||||
|
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/external/vtk-install/lib/cmake/vtk-9.6")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"VTK 未就位。请按 docs/ENV_SETUP_Windows.md §5.1 源码 Release 编 VTK 9.6.x 到 external/vtk-install。")
|
||||||
|
endif()
|
||||||
|
|
||||||
# VTK 9 必须指定 COMPONENTS,否则 VTK_LIBRARIES 为空、链接不到任何 VTK。
|
# VTK 9 必须指定 COMPONENTS,否则 VTK_LIBRARIES 为空、链接不到任何 VTK。
|
||||||
# 来自 VTK_DIR(external/vtk-install)。随渲染层增补模块(Volume/Filters 等)。
|
# 来自 VTK_DIR(external/vtk-install)。随渲染层增补模块(Volume/Filters 等)。
|
||||||
find_package(VTK REQUIRED COMPONENTS
|
find_package(VTK REQUIRED COMPONENTS
|
||||||
GUISupportQt
|
GUISupportQt
|
||||||
RenderingOpenGL2
|
RenderingOpenGL2
|
||||||
|
RenderingFreeType # 导航 gizmo 轴标签(vtkBillboardTextActor3D)运行时字形渲染:注册 FreeType 工厂
|
||||||
InteractionStyle
|
InteractionStyle
|
||||||
FiltersSources
|
FiltersSources
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# VTK 版本护栏:major.minor 须为 9.6(VTK_DIR 路径已带 vtk-9.6,越界基本是误装/误指)。
|
||||||
|
if(VTK_VERSION VERSION_LESS 9.6 OR NOT VTK_VERSION VERSION_LESS 9.7)
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"VTK 版本不匹配:找到 ${VTK_VERSION},需要 9.6.x。请按 docs/ENV_SETUP_Windows.md §5.1 用同一 Qt/工具集重编到 external/vtk-install。")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 工具集护栏(仅 WARN,绝不 FATAL):预编译 Qt 是 msvc2022(v143);VTK 必须与 app 用同一工具集编,
|
||||||
|
# 否则 std::map/std::string 跨界 ABI 崩溃(见 §10)。本机 VS2026-preview 与 v143 兼容、可正常构建。
|
||||||
|
if(MSVC AND NOT MSVC_TOOLSET_VERSION STREQUAL "143")
|
||||||
|
message(WARNING
|
||||||
|
"MSVC 工具集为 v${MSVC_TOOLSET_VERSION}(非 v143)。预编译 Qt 为 msvc2022(v143);请确保 external/vtk-install 的 VTK 与本 app 用【同一工具集】编,避免 ABI 不匹配崩溃。见 docs/ENV_SETUP_Windows.md §9.2。")
|
||||||
|
endif()
|
||||||
|
|
||||||
# 非 Qt 依赖(vcpkg),随分层逐步启用:
|
# 非 Qt 依赖(vcpkg),随分层逐步启用:
|
||||||
# find_package(GDAL CONFIG REQUIRED)
|
# find_package(GDAL CONFIG REQUIRED)
|
||||||
# find_package(PROJ CONFIG REQUIRED)
|
# find_package(PROJ CONFIG REQUIRED)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||||
"VCPKG_TARGET_TRIPLET": "x64-windows",
|
"VCPKG_TARGET_TRIPLET": "x64-windows",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
"CMAKE_PREFIX_PATH": "D:/Qt/6.11.1/msvc2022_64",
|
"CMAKE_PREFIX_PATH": "$env{QT_ROOT}",
|
||||||
"VTK_DIR": "${sourceDir}/external/vtk-install/lib/cmake/vtk-9.6"
|
"VTK_DIR": "${sourceDir}/external/vtk-install/lib/cmake/vtk-9.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
10
build.bat
10
build.bat
|
|
@ -42,6 +42,16 @@ if not exist "%CMAKE%" ( echo [build] cmake not found: "%CMAKE%" & exit /b 1 )
|
||||||
REM --- activate MSVC environment (cl / link / include / lib) ---
|
REM --- activate MSVC environment (cl / link / include / lib) ---
|
||||||
call "%VCVARS%" >nul
|
call "%VCVARS%" >nul
|
||||||
|
|
||||||
|
REM --- environment guardrails (fail loud & early, before cmake configure) ---
|
||||||
|
if not defined QT_ROOT (
|
||||||
|
echo [build] QT_ROOT is not set. Run: setx QT_ROOT "D:\Qt\6.11.1\msvc2022_64" ^(see docs/ENV_SETUP_Windows.md^), then reopen the terminal.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
if not defined VCPKG_ROOT (
|
||||||
|
echo [build] VCPKG_ROOT is not set. Run: setx VCPKG_ROOT "C:\dev\vcpkg" ^(see docs/ENV_SETUP_Windows.md^), then reopen the terminal.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
set "CMD=%~1"
|
set "CMD=%~1"
|
||||||
if "%CMD%"=="" set "CMD=app"
|
if "%CMD%"=="" set "CMD=app"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,87 @@
|
||||||
# Geopro 3.0 桌面客户端 — Windows 开发环境从零搭建指引
|
# Geopro 3.0 桌面客户端 — Windows 开发环境从零搭建指引
|
||||||
|
|
||||||
适用:Windows 10 22H2 / Windows 11,64 位,MSVC 2022 工具链。
|
适用:Windows 10 22H2 / Windows 11,64 位,**MSVC 2022(v143)工具链**。
|
||||||
目标:从一台干净的机器,搭到能 `cmake --build` 出可运行的 Qt6 + VTK9 桌面程序。
|
目标:从一台干净的机器,搭到 `build.bat app` 能编出可运行、且**不因 ABI 不匹配崩溃**的 Qt6 + VTK9 桌面程序。
|
||||||
|
|
||||||
> 配套设计文档:`docs/superpowers/specs/2026-06-07-geopro-desktop-m1-design.md`
|
> 最后核对:2026-07-01,已对齐 `CMakePresets.json` / `vcpkg.json` / 根 `CMakeLists.txt` / `build.bat` 的真实配置。
|
||||||
|
> ⚠️ 若你手里有更早版本的本文档:**§6.1/§7/§9 曾残留「全 vcpkg(Qt/VTK 也走 vcpkg)」的旧方案,与现状矛盾——以本版为准。** 现状是「方案②-修订」:**官方预编译 Qt + 源码编 VTK + vcpkg 只管非 Qt 依赖**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. 总览
|
## 0. 总览(方案②-修订)
|
||||||
|
|
||||||
> ⚠️ **构建方案已改定为「方案②-修订」**(经双专家评审 + 实机勘验)。本文档大部分步骤按此更新;**权威步骤以设计 §11 + `docs/superpowers/plans/2026-06-07-m1-phase0-spikes.md` 为准**。
|
**单一 Qt = 官方 MSVC 预编译 Qt**(`D:\Qt\6.11.1\msvc2022_64`)。**凡依赖 Qt 的组件都不走 vcpkg**(vcpkg 的 Qt 端口会再编一份 qtbase = 双份 Qt 冲突):
|
||||||
>
|
|
||||||
> **方案②-修订要点**:单一 Qt = **官方 MSVC 预编译 Qt**(`D:\Qt\6.11.1\msvc2022_64`)。**凡依赖 Qt 的组件(VTK/ADS/QtKeychain)都不走 vcpkg**(vcpkg 的 Qt 依赖端口会再编一份 qtbase = 双份冲突):VTK 用官方 Qt 源码编到 install 前缀;ADS/QtKeychain 走 FetchContent;仅非 Qt 依赖(GDAL/PROJ/OpenSSL/Eigen/...)走 vcpkg。
|
|
||||||
> **关键事实**:① 用户原装 `D:\Qt\6.11.1` 是 **MinGW 版**(MSVC 不可链),须在 Qt 维护工具里补装 **MSVC 2022 64-bit** kit;② VTK 无 MSVC 预编译,三方案都必须源码编;③ 本机 VS18 = MSVC 14.51,链官方 Qt(v143)属"新链旧",ABI 安全。
|
|
||||||
|
|
||||||
四块:① 编译器(VS18 / MSVC 14.51)② Git ③ vcpkg(仅非 Qt 依赖)④ 官方 MSVC Qt + 源码 VTK。
|
| 组件 | 来源 | 备注 |
|
||||||
|
|---|---|---|
|
||||||
|
| Qt 6.11.1(Core/Gui/Widgets/Network/Sql/Concurrent/OpenGL + tools) | **官方 MSVC 2022 64-bit kit**(§4) | 路径由预设 `CMAKE_PREFIX_PATH` 指定 |
|
||||||
|
| VTK 9.6.x(GUISupportQt/RenderingOpenGL2/RenderingFreeType/InteractionStyle/FiltersSources…) | **源码 Release 编 → `external/vtk-install`**(§5) | 必须对齐同一份 Qt + 同一工具集 + Release/`/MD` |
|
||||||
|
| ADS(Qt-Advanced-Docking-System 4.3.1) | **FetchContent 自动**(配置时拉,对接官方 Qt) | 无需手动 |
|
||||||
|
| QtKeychain v0.14.0 | **FetchContent 自动**(`BUILD_WITH_QT6`) | 无需手动 |
|
||||||
|
| Qwt 6.2(二维科学图表) | **手工克隆到 `external/qwt-src`**(§5.3,gitignored) | 缺失则详情页图表功能不编入 |
|
||||||
|
| vendored 3DGPRViewer(geopro_gpr3dv) | 仓库内 `external/gpr3dviewer`(随仓库) | 无需手动 |
|
||||||
|
| 非 Qt 依赖:eigen3 / gdal / gtest / nlohmann-json / openssl / proj | **vcpkg manifest**(`vcpkg.json`,有 baseline 锁版本) | 配置时自动拉 |
|
||||||
|
|
||||||
|
**ABI 铁律(否则运行时崩溃,见 §10)**:Qt(预编译 msvc2022) / VTK(你自己编) / app / 所有 FetchContent 组件,**必须同一工具集(v143)、同一运行时(`/MD`)、同一配置(全 Release)**。任何一处混 Debug/Release 或换工具集,`std::map`/`std::string` 跨界就崩。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Visual Studio 2022(MSVC + CMake + Ninja)
|
## 1. Visual Studio(MSVC v143 + CMake + Ninja)
|
||||||
|
|
||||||
1. 安装 **Visual Studio 2022 Community**(或更高)。
|
1. 安装 **Visual Studio 2022 Community**(或更高)。`build.bat` 也兼容 VS2026 preview,但**其工具集必须与预编译 Qt 的 msvc2022(v143) ABI 兼容**——若用 VS2026 的更新工具集,稳妥做法是**用同一工具集重编 VTK**(§5),别让 Qt(v143 预编译) 与 VTK/app(更新工具集) 混。
|
||||||
2. 勾选工作负载 **「使用 C++ 的桌面开发」(Desktop development with C++)**,确保包含:
|
2. 勾选工作负载 **「使用 C++ 的桌面开发」**,确保含:
|
||||||
- MSVC v143 - VS 2022 C++ x64/x86 生成工具
|
- MSVC v143 - VS 2022 C++ x64/x86 生成工具
|
||||||
- Windows 11 SDK(或 Windows 10 SDK)
|
- Windows 11 SDK(或 Windows 10 SDK)
|
||||||
- C++ CMake tools for Windows(自带 CMake + Ninja)
|
- C++ CMake tools for Windows(自带 CMake ≥3.21 + Ninja)
|
||||||
- C++ AddressSanitizer(用于 Debug Sanitizer,规约 §10.2)
|
- C++ AddressSanitizer(Debug Sanitizer 用)
|
||||||
3. 验证:开「x64 Native Tools Command Prompt for VS 2022」,运行:
|
3. 验证(开「x64 Native Tools Command Prompt for VS」):`cl` / `cmake --version`(≥3.21)/ `ninja --version`。
|
||||||
```
|
|
||||||
cl
|
|
||||||
cmake --version # 应 ≥ 3.21
|
|
||||||
ninja --version
|
|
||||||
```
|
|
||||||
|
|
||||||
> 之后所有 cmake/vcpkg 命令都在 **x64 Native Tools 命令行** 里跑(已设好 MSVC 环境变量)。
|
> `build.bat` 会用 `vswhere` 自动定位 VS 并激活 MSVC 环境,所以**日常构建直接跑 `build.bat` 即可**,不必手动开 x64 命令行。但 `cmake/ninja/cl` **不在 PATH**,手动跑 cmake 前需先激活 vcvars64。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Git
|
## 2. Git
|
||||||
|
|
||||||
1. 安装 [Git for Windows](https://git-scm.com/download/win)。
|
1. 安装 [Git for Windows](https://git-scm.com/download/win),`git --version` 验证。
|
||||||
2. 验证:`git --version`。
|
2. 本项目**已是 git 仓库**(无需 `git init`);正常 `git clone` 即可。
|
||||||
3. 本项目当前**尚未初始化 git 仓库**——首次提交前需 `git init`(见 §7)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. vcpkg(依赖管理)
|
## 3. vcpkg(仅非 Qt 依赖)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 选一个不含空格/中文的路径,例如 C:\dev
|
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg # 路径不含空格/中文
|
||||||
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg
|
|
||||||
C:\dev\vcpkg\bootstrap-vcpkg.bat
|
C:\dev\vcpkg\bootstrap-vcpkg.bat
|
||||||
|
setx VCPKG_ROOT "C:\dev\vcpkg" # 永久;新开终端生效
|
||||||
```
|
```
|
||||||
|
- **manifest 模式**:根 `vcpkg.json` 声明依赖 + `builtin-baseline` 锁版本,CMake 配置时自动拉取,**无需手动 `vcpkg install`**。
|
||||||
设环境变量(系统环境变量或当前会话):
|
- 实际依赖(勿加 Qt/VTK 进来):`eigen3, gdal, gtest, nlohmann-json, openssl, proj`。
|
||||||
```powershell
|
- **不要随意 `vcpkg x-update-baseline`**——baseline 已锁,改动会漂移依赖版本、可能引入 ABI 不一致。
|
||||||
$env:VCPKG_ROOT = "C:\dev\vcpkg"
|
- 首次会编 GDAL/PROJ 等,较久;可选配 `VCPKG_BINARY_SOURCES` 二进制缓存加速。
|
||||||
setx VCPKG_ROOT "C:\dev\vcpkg" # 永久(新开终端生效)
|
|
||||||
```
|
|
||||||
|
|
||||||
> 本项目用 **vcpkg manifest 模式**(`vcpkg.json`),不需要手动 `vcpkg install`;CMake 配置时按清单自动拉取。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Qt(官方 MSVC 预编译 kit)
|
## 4. Qt(官方 MSVC 2022 64-bit kit)
|
||||||
|
|
||||||
**用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用):
|
**必须是 MSVC kit**——若你原装的是 `mingw_64`,MSVC 下不可链:
|
||||||
|
|
||||||
1. `.\qt-online-installer-windows-x64-4.11.0.exe --mirror https://ftp.jaist.ac.jp/pub/qtproject`
|
1. 用官方在线安装器(或已装则开 `D:\Qt\MaintenanceTool.exe`)→ Add or remove components → 登录 Qt 账号。
|
||||||
2. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。
|
2. 展开 Qt → **Qt 6.11.1**,勾选 **MSVC 2022 64-bit**,安装。
|
||||||
3. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。
|
3. 完成后应存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`。
|
||||||
4. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。
|
4. **设 `QT_ROOT` 环境变量指向该 kit**(与 `VCPKG_ROOT` 同一套路,预设 `CMAKE_PREFIX_PATH=$env{QT_ROOT}` 读它,§6):
|
||||||
|
```powershell
|
||||||
|
setx QT_ROOT "<你的路径>\6.11.1\msvc2022_64" # 永久;新开终端生效
|
||||||
|
```
|
||||||
|
**全链路只此一份 Qt。** 版本仍须是 **6.11.1**(换版本可能与源码编的 VTK/ADS 不匹配)。
|
||||||
|
|
||||||
CMake 经 `CMAKE_PREFIX_PATH=D:/Qt/6.11.1/msvc2022_64` 找到它(见 §6 预设)。**全链路只此一份 Qt**。
|
> 若你的 Qt 装在别处/别的版本:**改 `QT_ROOT` 指向你的 `msvc2022_64` 即可**,无需动预设;但版本仍须是 6.11.1。
|
||||||
|
> 未设 `QT_ROOT` 时:`build.bat` 会立即报 `[build] QT_ROOT is not set...` 并退出;直接跑 cmake 则配置期 `FATAL_ERROR: QT_ROOT 未设置或无效`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 依赖来源(方案②-修订)
|
## 5. 源码依赖(手工准备)
|
||||||
|
|
||||||
| 类别 | 组件 | 来源 |
|
### 5.1 VTK 9.6.x 源码编到 `external/vtk-install`(用官方 Qt,Release)
|
||||||
|---|---|---|
|
|
||||||
| Qt | qtbase/widgets/network/sql/concurrent/opengl + tools | 官方 MSVC kit(§4) |
|
|
||||||
| VTK | vtk 9.3[qt,opengl](+gdal/proj 可选) | **源码编 → install 前缀**(§5.2) |
|
|
||||||
| Qt 依赖小件 | ADS、QtKeychain | **FetchContent 对接官方 Qt**(§6.2) |
|
|
||||||
| 非 Qt 依赖 | gdal/proj/openssl/eigen3/spdlog/fmt/nlohmann-json/gtest | **vcpkg**(下方 vcpkg.json) |
|
|
||||||
|
|
||||||
### 5.1 `vcpkg.json`(仅非 Qt 依赖)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "geopro-desktop",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"dependencies": ["gdal","proj","eigen3","spdlog","fmt","nlohmann-json","openssl","gtest"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **凡依赖 Qt 的(vtk[qt]/qtkeychain/qt-advanced-docking-system)绝不放进 vcpkg**——否则 vcpkg 会再编一份 qtbase = 双份 Qt 冲突(已核 `ports/vtk/vcpkg.json`)。
|
|
||||||
- `vcpkg x-update-baseline --add-initial-baseline` 锁版本(规约 §5.4)。
|
|
||||||
- 先配 `VCPKG_BINARY_SOURCES` 二进制缓存(实测当前为空),省 GDAL/PROJ 重编。
|
|
||||||
|
|
||||||
### 5.2 VTK 源码编到 install 前缀(用官方 Qt)
|
|
||||||
|
|
||||||
实机用 **VTK 9.6.2**(最新稳定,对 Qt 6.11 兼容最好),源码/构建全放 **D:**(C: 仅剩 ~1GB)。脚本见 `external/build_vtk.bat`(已 .gitignore),要点:
|
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
call "<VS>\VC\Auxiliary\Build\vcvars64.bat"
|
call "<VS>\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
|
@ -114,18 +95,32 @@ cmake -S D:\dev\vtk-src -B D:\dev\vtk-build -G Ninja ^
|
||||||
-D CMAKE_INSTALL_PREFIX=D:/Git/lanbingtech/geopro/external/vtk-install
|
-D CMAKE_INSTALL_PREFIX=D:/Git/lanbingtech/geopro/external/vtk-install
|
||||||
cmake --build D:\dev\vtk-build --target install
|
cmake --build D:\dev\vtk-build --target install
|
||||||
```
|
```
|
||||||
完成后 `external/vtk-install/lib/cmake/vtk-9.6` 供 `find_package(VTK)`(已在 `CMakePresets.json` 设 `VTK_DIR`)。
|
- 完成后 `external/vtk-install/lib/cmake/vtk-9.6` 供 `find_package(VTK)`(`CMakePresets.json` 里 `VTK_DIR` 已指向它)。
|
||||||
> 注:VTK 用 **Release** 编。因此**冒烟程序也用 `msvc-release` 预设构建**,以匹配 Release VTK + Release Qt(避免 `/MD` vs `/MDd` 混链)。需要 Debug 调试 VTK 时再出一份 Debug VTK。
|
- **必须 Release + 与本项目相同的工具集(v143)编**;因此**本项目也用 `msvc-release` 构建**,避免 `/MD` vs `/MDd` 或 Debug/Release 混链(→ §10 崩溃)。需要调试 VTK 时另出一份 Debug VTK,且此时 app 也须整套 Debug。
|
||||||
|
- 根 `CMakeLists.txt` 里 `find_package(VTK REQUIRED COMPONENTS ...)` **必须列组件**(否则 `VTK_LIBRARIES` 为空、链不到);当前组件:`GUISupportQt / RenderingOpenGL2 / RenderingFreeType / InteractionStyle / FiltersSources`(随渲染层增补)。
|
||||||
|
|
||||||
|
### 5.2 ADS / QtKeychain(FetchContent,自动)
|
||||||
|
|
||||||
|
无需手动——根 `CMakeLists.txt` 用 `FetchContent` 拉 **ADS 4.3.1** 与 **QtKeychain v0.14.0**,对接同一份官方 Qt(`BUILD_WITH_QT6=ON`)。首次配置会 clone,需能访问 GitHub。
|
||||||
|
|
||||||
|
### 5.3 Qwt 6.2(手工克隆到 `external/qwt-src`)
|
||||||
|
|
||||||
|
二维科学图表(数据集详情散点/等值线)依赖 Qwt。`external/qwt-src` 已 gitignore,须自备:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 克隆/解压 Qwt 6.2 源码到 external/qwt-src,使 external/qwt-src/src 存在
|
||||||
|
git clone --branch qwt-6.2 https://git.code.sf.net/p/qwt/git external/qwt-src
|
||||||
|
```
|
||||||
|
- CMake 检测到 `external/qwt-src/src` 才 include `cmake/qwt.cmake` 编 Qwt;**缺失则相关图表功能不编入**(不报错,但详情图表缺失)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. CMake 接线
|
## 6. CMake 接线(现状,勿照旧文档)
|
||||||
|
|
||||||
### 6.1 `CMakePresets.json`(项目根)
|
### 6.1 `CMakePresets.json`(真实内容)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 3,
|
|
||||||
"configurePresets": [
|
"configurePresets": [
|
||||||
{
|
{
|
||||||
"name": "msvc-debug",
|
"name": "msvc-debug",
|
||||||
|
|
@ -134,112 +129,94 @@ cmake --build D:\dev\vtk-build --target install
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Debug",
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
"VCPKG_TARGET_TRIPLET": "x64-windows",
|
||||||
|
"CMAKE_PREFIX_PATH": "$env{QT_ROOT}",
|
||||||
|
"VTK_DIR": "${sourceDir}/external/vtk-install/lib/cmake/vtk-9.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ "name": "msvc-release", "inherits": "msvc-debug",
|
||||||
"name": "msvc-release",
|
|
||||||
"inherits": "msvc-debug",
|
|
||||||
"binaryDir": "${sourceDir}/build/release",
|
"binaryDir": "${sourceDir}/build/release",
|
||||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
|
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
> 全 vcpkg 方案下**不设 `CMAKE_PREFIX_PATH` 指向官方 Qt**——vcpkg 工具链接管 Qt 查找(避免双 Qt)。
|
- **预设确实设 `CMAKE_PREFIX_PATH`(官方 Qt,读 `$env{QT_ROOT}`)+ `VTK_DIR`(源码编的 VTK,仓库相对、无需环境变量)**——这是「方案②-修订」的核心,别按旧文档去掉它。`QT_ROOT` 未 setx 时配置期会 FATAL(§4)。
|
||||||
|
- vcpkg 只经 `CMAKE_TOOLCHAIN_FILE` 管非 Qt 依赖;triplet `x64-windows`。
|
||||||
|
|
||||||
### 6.2 顶层 `CMakeLists.txt`(骨架)
|
### 6.2 根 `CMakeLists.txt` 要点
|
||||||
|
|
||||||
```cmake
|
- `find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)`。
|
||||||
cmake_minimum_required(VERSION 3.21)
|
- `find_package(VTK REQUIRED COMPONENTS GUISupportQt RenderingOpenGL2 RenderingFreeType InteractionStyle FiltersSources)`(**必须列组件**)。
|
||||||
project(geopro_desktop LANGUAGES CXX)
|
- MSVC flags:`/utf-8 /MP /W4 /permissive-`;非 Debug 配置产 PDB(`/Zi` + `/DEBUG` + `/OPT:REF,ICF`)。
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
- ADS/QtKeychain 经 FetchContent;Qwt 经 `cmake/qwt.cmake`(存在才编)。
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
- 视图层用 **`QVTKOpenGLStereoWidget`**(QOpenGLWidget 系,ADS reparent 友好)。
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTORCC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)
|
---
|
||||||
find_package(VTK REQUIRED) # 含 GUISupportQt(vtk[qt])→ QVTKOpenGLStereoWidget
|
|
||||||
find_package(GDAL CONFIG REQUIRED)
|
|
||||||
find_package(PROJ CONFIG REQUIRED)
|
|
||||||
find_package(Eigen3 CONFIG REQUIRED)
|
|
||||||
find_package(spdlog CONFIG REQUIRED)
|
|
||||||
find_package(nlohmann_json CONFIG REQUIRED)
|
|
||||||
find_package(OpenSSL REQUIRED)
|
|
||||||
# find_package(Qt6Keychain CONFIG REQUIRED) # qtkeychain
|
|
||||||
# ADS:vcpkg 端口验证通过后 find_package;否则用下方 FetchContent
|
|
||||||
|
|
||||||
add_subdirectory(src)
|
## 7. 首次配置、编译、运行、部署
|
||||||
enable_testing()
|
|
||||||
add_subdirectory(tests)
|
**日常直接用 `build.bat`(推荐)**:
|
||||||
```
|
```
|
||||||
|
build.bat app # 配置(首次)+ 增量编 geopro_desktop(Release)
|
||||||
> VTK 链接用 `vtk_module_autoinit`;视图层用 **`QVTKOpenGLStereoWidget`**(QOpenGLWidget 系,ADS reparent 友好,见设计 §K-9),不用 native 版。
|
build.bat test # 编 + ctest 跑单测
|
||||||
|
build.bat run # 编 + 启动
|
||||||
> **ADS 备选引入**(若 vcpkg 端口不可用,spike 阶段确定):
|
build.bat rebuild # 强制 --clean-first 全量重编(增量疑似漏编时用)
|
||||||
> ```cmake
|
build.bat configure # CMakeLists 改动后强制重跑 configure
|
||||||
> include(FetchContent)
|
|
||||||
> FetchContent_Declare(ads GIT_REPOSITORY https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System.git GIT_TAG <tag>)
|
|
||||||
> FetchContent_MakeAvailable(ads)
|
|
||||||
> ```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 首次配置、编译、运行
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 在 x64 Native Tools 命令行、项目根目录
|
|
||||||
git init # 首次:初始化仓库
|
|
||||||
vcpkg x-update-baseline --add-initial-baseline
|
|
||||||
|
|
||||||
cmake --preset msvc-debug # 首次会编译 VTK 等,耗时较长
|
|
||||||
cmake --build build/debug
|
|
||||||
|
|
||||||
# 运行(VTK/Qt 的 dll 需在 PATH 或同目录)
|
|
||||||
.\build\debug\src\app\geopro_desktop.exe
|
|
||||||
```
|
```
|
||||||
|
- `build.bat` 固定 `--preset msvc-release`、`build/release`,自动激活 MSVC 环境。exe:`build/release/src/app/geopro_desktop.exe`。
|
||||||
|
- 首次会拉 vcpkg 依赖 + FetchContent(ADS/QtKeychain),较久;VTK/Qt/Qwt 须已按 §4/§5 就位。
|
||||||
|
|
||||||
**运行期 DLL 部署(单一 vcpkg 链路)**:
|
**运行期 DLL 部署(官方 Qt,非 vcpkg)**:
|
||||||
- 全部 dll(含 Qt)来自 vcpkg:`build/vcpkg_installed/x64-windows/(debug/)bin`。
|
- Qt6*.dll + plugins:用**官方 Qt 的 `windeployqt`**(`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe`)对齐**同一份官方 Qt** 到 exe 目录。
|
||||||
- 用 CMake `TARGET_RUNTIME_DLLS` + `add_custom_command(POST_BUILD)` 自动拷贝到 exe 目录(实现阶段加)。
|
- VTK*.dll:从 `external/vtk-install/bin` 拷到 exe 目录(或加进 PATH)。
|
||||||
- **不混用官方 Qt 的 `windeployqt`**——本方案 Qt 来自 vcpkg,混用会拷错版本造成双 Qt 冲突。
|
- **只保证 exe 目录一份 Qt6*.dll(无双 Qt)**;勿混入别处/vcpkg 的 Qt。
|
||||||
- Qt plugins(platforms/imageformats/sqldrivers 等)由 vcpkg 部署脚本处理;如缺再用 vcpkg 安装树里的 `windeployqt` 对齐同一份 Qt。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. AI 编码上下文基础设施(规约 §10.1,强烈建议先建)
|
## 8. AI/IDE 上下文(clangd)
|
||||||
|
|
||||||
项目根放:
|
- `.clangd`:`CompileFlags.CompilationDatabase: build/release`(对齐 `build.bat` 的主构建目录;若你主要用 Debug 则指 `build/debug`)。`CMAKE_EXPORT_COMPILE_COMMANDS=ON` 会在该目录产 `compile_commands.json`。
|
||||||
|
- VS Code 用 **clangd** 扩展(禁用微软 C++ IntelliSense 避免冲突)。
|
||||||
- `.clang-format`:统一风格(基于 LLVM/Google + 团队微调)。
|
|
||||||
- `.clangd`:
|
|
||||||
```yaml
|
|
||||||
CompileFlags:
|
|
||||||
CompilationDatabase: build/debug
|
|
||||||
```
|
|
||||||
使 clangd 读取 `compile_commands.json`,给 AI/IDE 精确类型上下文。
|
|
||||||
- VS Code 装 **clangd** 扩展(禁用微软 C++ IntelliSense 避免冲突),或 CLion 直接用 CMake。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 验证清单
|
## 9. 构建环境兼容性检查清单(交付/接手前逐项核对)
|
||||||
|
|
||||||
- [ ] `cl` / `cmake` / `ninja` / `git` 命令可用
|
> 目的:把「配置错→运行时随机崩溃」变成「一眼可查」。**任何一项不满足都可能导致 §10 的 `std::map`/`std::string` ABI 崩溃。**
|
||||||
- [ ] `VCPKG_ROOT` 已设
|
|
||||||
- [ ] `vcpkg.json` 含 qtbase + vtk[qt](共用一份 Qt),preset **未**指向官方 Qt
|
### 9.1 前置就位
|
||||||
- [ ] `cmake --preset msvc-debug` 成功(Qt+VTK 已拉取编译,首次较久)
|
- [ ] `cl` / `cmake`(≥3.21) / `ninja` / `git` 可用;`VCPKG_ROOT` 已设。
|
||||||
- [ ] `cmake --build` 出 exe
|
- [ ] `QT_ROOT` 已 setx 指向 **6.11.1 `msvc2022_64`** kit(MSVC,非 MinGW);`$QT_ROOT/lib/cmake/Qt6` 在(预设 `CMAKE_PREFIX_PATH=$env{QT_ROOT}` 读它;未设则配置期 FATAL)。
|
||||||
- [ ] exe 能起一个空 Qt 窗 + 一个 `QVTKOpenGLStereoWidget` 渲染窗(冒烟测试)
|
- [ ] `external/vtk-install/lib/cmake/vtk-9.6` 存在(VTK 已源码编 install)。
|
||||||
- [ ] 部署后 exe 目录只有一份 Qt6*.dll(无双 Qt)
|
- [ ] `external/qwt-src/src` 存在(如需详情页图表)。
|
||||||
- [ ] `compile_commands.json` 生成,clangd 正常索引
|
- [ ] `vcpkg.json` 的 `builtin-baseline` 未被改动。
|
||||||
|
|
||||||
|
### 9.2 ABI 一致性(最关键,防崩溃)
|
||||||
|
- [ ] **单一配置**:Qt(预编译) / VTK(你编) / app / ADS / QtKeychain / Qwt **全 Release**(或全 Debug),**绝不混**。日常一律 `build.bat`(msvc-release)。
|
||||||
|
- [ ] **同一工具集**:VTK/app 用的 MSVC 工具集与预编译 Qt 的 **v143(msvc2022)** ABI 兼容;若用 VS2026-preview 工具集,**用它重编 VTK**,别让 Qt(v143) 与 VTK/app(更新集) 混。
|
||||||
|
- [ ] **同一运行时**:全部 `/MD`(Release 动态 CRT),`_ITERATOR_DEBUG_LEVEL=0`。**绝不把 Release 的 Qt/VTK 链进 Debug 的 app**(Debug 是 `/MDd` + IDL=2,STL 布局不同)。
|
||||||
|
- [ ] **VTK 来源正确**:是**你按 §5.1 源码 Release 编、对齐本 Qt** 的那份;**不是** vcpkg 的 VTK、不是别处/别配置的 VTK。
|
||||||
|
- [ ] **单一 Qt**:exe 目录/PATH 只有一份 Qt6*.dll,无双 Qt。
|
||||||
|
|
||||||
|
### 9.3 干净构建验证
|
||||||
|
- [ ] 删 `build/` 后 `build.bat app` 从零配置**无错**(尤其无「找不到 Qt6/VTK」「add_subdirectory 目录不存在」)。
|
||||||
|
- [ ] `build.bat test` → ctest 全绿。
|
||||||
|
- [ ] 启动 app、点对象树、渲染视图**不崩**(§10 的崩溃即出现在这里)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 与设计文档对应的 spike 门槛
|
## 10. 已知崩溃签名 → 根因速查
|
||||||
|
|
||||||
本指引服务于设计 §15 的第一周 spike:① 全 vcpkg 构建/部署打通(本文);② ADS + `QVTKOpenGLStereoWidget` 浮动/重停靠不黑屏;③ 真实样本跑通 banded contour。三者通过再进入完整实现计划。
|
| 现象 | 根因 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| **点对象树/渲染时,崩在 `std::_Tree::_Find_lower_bound`(`std::map`/`set` 查找),`_Myhead` 是 `0xFFFF...` 之类垃圾值,读取访问冲突** | **STL ABI 不匹配**:Debug/Release 混链、`/MD` vs `/MDd`、`_ITERATOR_DEBUG_LEVEL` 不一致、或工具集不匹配(最常见:**别处/别配置的 VTK/Qt**,或 **Debug app 链 Release 依赖**) | 按 §9.2 逐项核对;**删 `build/` 全 Release 干净重建**;确认 VTK 是 §5.1 那份 |
|
||||||
|
| 配置期 `FATAL_ERROR: QT_ROOT 未设置或无效` / `build.bat` 报 `[build] QT_ROOT is not set` | 未 setx `QT_ROOT` 或指向的 kit 不含 `lib/cmake/Qt6` | `setx QT_ROOT "<你的Qt>\6.11.1\msvc2022_64"` 后**重开终端**(§4) |
|
||||||
|
| 配置期 `FATAL_ERROR: Qt 版本不匹配` / `VTK 版本不匹配` / `VTK 未就位` | 装了非 6.11.x Qt、非 9.6.x VTK,或 `external/vtk-install` 缺失 | 按 §4/§5 装对版本/就位 VTK |
|
||||||
|
| 链接期报「找不到 Qt6::/VTK::」 | 预设 `CMAKE_PREFIX_PATH`(`$env{QT_ROOT}`)/`VTK_DIR` 指向不存在 | 按 §4/§5 就位或修正 `QT_ROOT` |
|
||||||
|
| 配置报「add_subdirectory ... 不是已存在目录」 | 引用了未随仓库的目录(如误提交本地临时工具目录) | 从 `CMakeLists.txt` 去掉该引用,或补齐该目录 |
|
||||||
|
| 详情页图表缺失但不报错 | `external/qwt-src` 未就位(§5.3) | 克隆 Qwt 后重配置 |
|
||||||
|
| 双 Qt / 起不来找不到 platform 插件 | 混入了 vcpkg/别处的 Qt dll | 只用官方 `windeployqt` 对齐单一 Qt |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*遇到具体报错(VTK/Qt/GDAL 链接、ADS 端口、双 Qt)按规约 §10.4 由工程师复核;AI 协助按 build-error-resolver 流程逐条解决。*
|
*遇到具体报错按规约由工程师复核;AI 协助按 build-error-resolver 流程逐条解决。*
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,256 @@
|
||||||
|
# VTK 视图重构:合并数据集单栏 + 动态段 + 图标工具条 + 2D 平面底图 — Spec(2026-06-30)
|
||||||
|
|
||||||
|
> 分支起点:`main`(PR #9 已合并)。职责范围:VTK 视图左侧数据集栏(面板结构 + 段交互 + 2D/3D 共存渲染 + 底图)。
|
||||||
|
> 本 spec 替代 2026-06-26「二维分析:锁定俯视」模型的相机/显隐部分(见 §10 迁移说明)。
|
||||||
|
|
||||||
|
## 0. 一句话目标
|
||||||
|
|
||||||
|
把「三维分析 + 二维分析」两个 tab 合并成**一个无标题的单列数据集栏**:2D 与 3D 数据集用**一致的分段组织**同列呈现;段按数据有无**动态显隐**;段操作改**响应式图标工具条**;2D 数据以**按类型一块平面**的方式与 3D 体/帘面在**同一个自由透视场景**里共存。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与现状(接手必读)
|
||||||
|
|
||||||
|
- **面板**:`ColumnDrawer` = 左侧抽屉,`QTabWidget` 两 tab:
|
||||||
|
- 三维分析 `CategoryAnalysisTab`:`QScrollArea` 竖堆 `CategorySection`×4(电阻率/视电阻率/瞬变/三维体)。
|
||||||
|
- 二维分析 `Column2DDataset`:平铺树 + 底图下拉 + 2D视图模式下拉 + 自定义Z 滑块。
|
||||||
|
- 切 tab 发 `analysisModeChanged(bool is2D)`。
|
||||||
|
- **段** `CategorySection`:段头(chevron+标题 | 新增三维体[反演类] | 导入雷达[voxel])+ 段体(**固定显示**的筛选行[日期范围+装置类型] + 可勾选数据树)。
|
||||||
|
- **分类**:3D 用 `splitByCategory()`(`categoryConfigs()` 4 段);2D 用 `splitByDimension().dim2D`(当前仅 `dd_trajectory_data`)。
|
||||||
|
- **渲染**:
|
||||||
|
- 2D 轨迹 → `MapLineActor`(橙色 `vtkPolyLine`),经 `VtkSceneController::set2DPlacement(mode,z)` 摆到**单一全局 Z**(5 模式:0关/1 Z=0/2顶+50/3底-50/4自定义)。已有逐 ds 的 Z 拖动偏移 `mapLineZOffset_`。
|
||||||
|
- `VtkSceneView::setAnalysisMode2D(is2D)`:切 tab 时**按维度翻 actor 可见标志** + 相机锁定近俯视 + `VtkViewToolbar` 禁 6 向视图。
|
||||||
|
- 底图 `TileBasemap`(单例):天地图 WMTS(卫星 `img_w`→`buildWarped` 带高程地形 / 矢量 `vec_w`→`buildFlat` 纯平面),透明度**固定 0.55**,瓦片范围 = `dataHorizontalRadius()×10` 钳 `[2000,30000]m`,置 Z=0。`buildFlat` 已能纯平贴矢量瓦片。
|
||||||
|
- **顶部工具条** `TopBar`:视图/项目管理/业务工具/**设备** 四菜单。
|
||||||
|
- **渲染区竖排工具栏** `VtkViewToolbar`:段1 `Gear`(坐标轴设置) → 分隔线 → 6向视图 → 缩放。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 已确认的关键决策(与用户逐条确认)
|
||||||
|
|
||||||
|
1. **单一自由场景共存**:合并删 tab 后,**取消**「锁定近俯视相机 + 按维度自动显隐」。场景恒为自由透视,勾选的 2D 平面与 3D 体/帘面**同时可见**。
|
||||||
|
2. **底图 = 1 个 3D 底图 + N 个 2D 底图**:所有三维数据**共用一个** 3D 底图(现状 `TileBasemap`);每个 **2D 类型(段)** 一块**独立平面底图**(N = 2D 段数,非每条 ds 一张)。
|
||||||
|
3. **默认勾选**:沿用现有「直接挂项目下的 ds 默认进 VTK」逻辑,本次只保证动态显隐段时不破坏它。
|
||||||
|
4. **2D z 值按类型一块平面**:每个 2D 类型一块平面 + 一个 z 滑块;平面初始 z = 该类型**第一个被勾选 ds** 的 z;同类型其余 ds 投影到此平面。
|
||||||
|
5. **3D 底图控件移到 `VtkViewToolbar`**(Gear 之后),不在 3D 段上。
|
||||||
|
6. **「导入雷达」移到 `TopBar` 设备菜单**(临时测试功能,后续整体移除)。
|
||||||
|
7. **`view2DMode` 5 模式下拉废弃**,2D 高度完全由「z值」滑块替代。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 架构方案
|
||||||
|
|
||||||
|
**采用方案 A:统一单列 `DatasetColumn` + 类型抽象(§5)。**
|
||||||
|
|
||||||
|
- 用**一份类目描述符目录 `categoryCatalog()`**(§5,每类型一份 `CategoryDescriptor`:分类/筛选/操作/渲染策略)同时驱动 3D/2D 段;`CategorySection` 按描述符建筛选器与图标条;`VtkSceneController` 按描述符的 `renderStrategyId` 查可插拔渲染策略(§5.4)渲染。**消费方不再 `if dimension/ddCode` 散判**。
|
||||||
|
- 删除 `Column2DDataset` 与 `QTabWidget`;`ColumnDrawer` 承载单个 `DatasetColumn`,保留折叠开关。
|
||||||
|
- 取舍:复用已验证的勾选保留/折叠/spinner/结构树逻辑;改动集中在描述符目录、段头工具条(OpKind 映射)、渲染策略注册表。
|
||||||
|
|
||||||
|
(备选 B「两 widget 去 tab 竖堆」因 2D/3D 段不一致、重复逻辑被否;C「全重写」工作量过大被否。)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 面板结构(§1 重构)
|
||||||
|
|
||||||
|
- `ColumnDrawer`:去 `QTabWidget` + `Column2DDataset`;改持 `DatasetColumn`;**移除** `analysisModeChanged` 信号链。折叠开关保留。
|
||||||
|
- `DatasetColumn`(原 `CategoryAnalysisTab` 改名/改造):`QScrollArea` 竖堆 N 个 `CategorySection`,**无栏目标题**。
|
||||||
|
- **动态显隐**:段 bucket 为空 → 段 `hide()`;非空 → `show()`。三维体段同理(默认空→默认不显示)。`relayoutSections()`/stretch 逻辑只对可见段生效。
|
||||||
|
- **空面板占位**:所有段均空时,滚动区中央显示提示语占位(文案:「请在左侧对象树勾选测线 / 数据集」);任一段非空则隐藏占位。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 类型抽象(扩展契约 —— 本 spec 的架构基石)
|
||||||
|
|
||||||
|
> 目标:**接入一个新 ds 类型 = 实现一份描述符(必要时再补一个渲染策略 / 一个操作 / 一个筛选器)**,无论它「按什么规则接入、有什么操作、怎么渲染」,UI 层与渲染层都只消费抽象、不再 `if dimension/ddCode` 散判。这是「数据集栏目」的统一规范,所有现存 5 类与未来新类都走它。
|
||||||
|
|
||||||
|
### 5.1 类目描述符 `CategoryDescriptor`(data 层,纯 C++,无 Qt/VTK)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
enum class SceneKind { Volume3D, Curtain3D, Plane2D }; // 渲染语义 / 共存规则
|
||||||
|
enum class FilterKind { DateRange, ArrayType }; // 筛选器契约(可扩展)
|
||||||
|
enum class OpKind { GenerateVolume, Filter, PlaneZ, Basemap }; // 段操作契约(可扩展)
|
||||||
|
|
||||||
|
struct CategoryDescriptor {
|
||||||
|
std::string id; // "resistivity"/"apparent"/"transient"/"voxel"/"trajectory" ...
|
||||||
|
std::string title; // 段标题
|
||||||
|
SceneKind sceneKind; // 渲染语义
|
||||||
|
std::function<bool(const DsRow&)> classify; // 轴1 数据来源/分类("无论按什么规则接入")
|
||||||
|
std::vector<FilterKind> filters; // 轴2 本段筛选器(顺序=显示顺序)
|
||||||
|
std::vector<OpKind> operations; // 轴3 段头图标操作(顺序=显示顺序)
|
||||||
|
std::string renderStrategyId; // 轴4 渲染策略键(解析到注册表,见 §5.4)
|
||||||
|
};
|
||||||
|
|
||||||
|
// classify 便捷构造器(覆盖现有按 ddCode / dsTypeCode 接入的常见情形;任意复杂规则可直接写 lambda)
|
||||||
|
std::function<bool(const DsRow&)> byDdCode(std::initializer_list<std::string> codes);
|
||||||
|
std::function<bool(const DsRow&)> byDsTypeCode(std::initializer_list<std::string> codes);
|
||||||
|
|
||||||
|
const std::vector<CategoryDescriptor>& categoryCatalog(); // 有序目录,取代 categoryConfigs()
|
||||||
|
} // namespace geopro::data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 目录定义(catalog,取代 `categoryConfigs()`)
|
||||||
|
|
||||||
|
| id | title | sceneKind | classify | filters | operations | renderStrategyId |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| resistivity | 电阻率数据 | Curtain3D | `byDsTypeCode({"ERT platform inversion data"})` | DateRange,ArrayType | GenerateVolume,Filter | `"curtain"` |
|
||||||
|
| apparent | 视电阻率数据 | Curtain3D | `byDsTypeCode({"visual resistivity data"})` | DateRange,ArrayType | GenerateVolume,Filter | `"curtain"` |
|
||||||
|
| transient | 瞬变电磁数据 | Curtain3D | `byDsTypeCode({"DD TRANSIENT ELECTROMAGNETIC INVERSION"})` | DateRange | GenerateVolume,Filter | `"curtain"` |
|
||||||
|
| voxel | 三维体 | Volume3D | `byDdCode({"dd_voxel"})`(mock 注入,见 §10) | DateRange | Filter | `"volume"` |
|
||||||
|
| trajectory | 轨迹数据 | Plane2D | `byDdCode({"dd_trajectory_data"})` | DateRange | PlaneZ,Filter,Basemap | `"plane2d"` |
|
||||||
|
|
||||||
|
段顺序即表序(电阻率→…→轨迹)。分流:`splitByCategory(rows)` 改为遍历 catalog,对每行命中**首个** `classify(row)==true` 的描述符即归入该段(保留原顺序)。`Column2DDataset` / `splitByDimension` 退役;旧 `categoryConfigs()`/`CategorySpec` 由 `categoryCatalog()`/`CategoryDescriptor` 取代。
|
||||||
|
|
||||||
|
### 5.3 消费方只认抽象
|
||||||
|
|
||||||
|
- `CategorySection(descriptor)`:按 `descriptor.filters` 建筛选器(`FilterKind→UI` 映射一处)、按 `descriptor.operations` 建图标条(`OpKind→按钮+信号` 映射一处,见 §6/§7)。
|
||||||
|
- `DatasetColumn`:遍历 `categoryCatalog()` 建段、用 `descriptor.classify` 路由数据(即 `splitByCategory`)。
|
||||||
|
- `VtkSceneController`:勾选某 ds → 查其所属描述符 → `renderStrategyId` → 渲染策略 `add/remove` + 首勾/全消 `onTypeActivated/Deactivated`(见 §5.4、§8)。
|
||||||
|
|
||||||
|
### 5.4 可插拔渲染策略 `IDatasetRenderStrategy`(controller/render 层)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace geopro::controller {
|
||||||
|
class IDatasetRenderStrategy {
|
||||||
|
public:
|
||||||
|
virtual ~IDatasetRenderStrategy() = default;
|
||||||
|
virtual void add(const std::string& typeId, const std::string& dsId) = 0; // 异步加载+入场
|
||||||
|
virtual void remove(const std::string& dsId) = 0;
|
||||||
|
// 每类型场景资源生命周期(可选):本类型首个 ds 入场 / 全部离场
|
||||||
|
virtual void onTypeActivated(const std::string& typeId) {}
|
||||||
|
virtual void onTypeDeactivated(const std::string& typeId) {}
|
||||||
|
};
|
||||||
|
// 注册表:renderStrategyId(字符串键) → 策略实例。当前 3 实现:
|
||||||
|
// "volume" VolumeRenderStrategy —— 体素/雷达体(包现 isVolumeDataset 分支)
|
||||||
|
// "curtain" CurtainRenderStrategy —— 反演帘面(包现 dd_section 等分支)
|
||||||
|
// "plane2d" Plane2DRenderStrategy —— 2D 折线落类型平面 + 平面底图(封装 §8.2 平面 z + §9.2 底图)
|
||||||
|
} // namespace geopro::controller
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键**:2D 的全部特殊性(按类型平面 z 生命周期 + N 个平面底图)**封死在 `Plane2DRenderStrategy` 一个类内**(内含 `PlaneZRegistry` + 平面底图管理);3D 两个策略只是包住现有渲染分支。控制器主流程不再含维度/ddCode 分支,只做「查描述符 → 取策略 → 调用」。
|
||||||
|
|
||||||
|
### 5.5 「接入一个新 ds 类型」标准动作(即本规范)
|
||||||
|
|
||||||
|
| 场景 | 要做的 |
|
||||||
|
|---|---|
|
||||||
|
| 新类型、**沿用**已有筛选/操作/渲染 | **只加一条 `CategoryDescriptor`**(纯数据),完。 |
|
||||||
|
| 新类型、要**新渲染方式** | 加描述符 + 实现一个 `IDatasetRenderStrategy` 并注册(新 `renderStrategyId`)。 |
|
||||||
|
| 新类型、要**新操作** | `OpKind` 加一项 + `CategorySection` 加该 kind→UI 的**一处**映射;描述符 `operations` 列上。 |
|
||||||
|
| 新类型、要**新筛选器** | `FilterKind` 加一项 + `CategorySection` 加**一处**映射;描述符 `filters` 列上。 |
|
||||||
|
|
||||||
|
数据驱动优先;只有真出现「新渲染/新操作/新筛选器」才动对应那**一个**扩展点,且改动收敛在单一位置,不扩散。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 响应式图标工具条(新建 `SectionIconBar`)
|
||||||
|
|
||||||
|
- 段头右侧承载图标工具条:**默认最多显示 3 个图标**;图标总数超 3,**或段宽被挤压放不下时**,**右侧图标依次收进末尾「…」下拉菜单**。`resizeEvent` 动态重算可见数 —— 这是**必须实现并可验证**的行为:即使图标 ≤3,当栏位宽度收窄到放不下时,右侧图标也要实时折进「…」,栏位变宽再弹回。
|
||||||
|
- 每个图标 = `QToolButton`(autoRaise + glyph + tooltip),点击触发对应操作(部分弹 popup)。
|
||||||
|
- **图标集由描述符 `operations`(`OpKind` 列表)驱动**,不是按维度硬编码。`CategorySection` 内有一处 `OpKind→(图标 glyph + tooltip + 点击/popup)` 映射表;新增操作只加一项映射(§5.5)。当前 catalog 各段对应:
|
||||||
|
| 段 | `operations`(左→右,右侧优先收进…) |
|
||||||
|
|---|---|
|
||||||
|
| 3D 反演(电阻率/视电阻率/瞬变) | `GenerateVolume`、`Filter` |
|
||||||
|
| 3D 三维体 | `Filter` |
|
||||||
|
| 2D 轨迹 | `PlaneZ`、`Filter`、`Basemap` |
|
||||||
|
- 注:当前各段图标 ≤3,「数量超 3」分支暂不触发,但**「宽度挤压」分支必须工作**(窄栏即折叠);两个分支都要实现(后续图标会增加,数量分支随之生效)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 段内操作行为(每个 = 一个 `OpKind`)
|
||||||
|
|
||||||
|
> 下列每个操作对应 §5 的一个 `OpKind`;`CategorySection` 的 `OpKind→UI` 映射表据描述符 `operations` 装配。`PlaneZ`/`Basemap` 的渲染落点封装在 `Plane2DRenderStrategy`(§5.4/§8.2/§9.2)。
|
||||||
|
|
||||||
|
### 7.1 筛选 `OpKind::Filter`(通用,D2+D3)
|
||||||
|
- 现「固定显示的筛选行(日期范围 + 装置类型[仅 ERT])」改为**默认折叠**(段体内不占位)。
|
||||||
|
- 点「筛选」图标 → 展开筛选行;再点 → 收起(toggle)。筛选逻辑(`passesFilters`/`rebuildList`)不变。
|
||||||
|
|
||||||
|
### 7.2 新增三维体 `OpKind::GenerateVolume`(仅 3D 反演段)
|
||||||
|
- 保持原功能(发 `generateVolumeRequested`),入口从文字按钮改图标。
|
||||||
|
|
||||||
|
### 7.3 z值 `OpKind::PlaneZ`(仅 2D 段)
|
||||||
|
- 点图标弹 popup:一个滑块,整体上下移动**该 2D 类型那块平面**的 z。
|
||||||
|
- 初值 = 该类型第一个被勾选 ds 的 z(见 §8.2);范围按场景高程量级合理取(实现期定)。
|
||||||
|
|
||||||
|
### 7.4 底图 `OpKind::Basemap`(仅 2D 段)
|
||||||
|
- 点图标弹 popup:【底图类型:矢量平面(默认) / 无】+【透明度滑块(默认 50%)】。
|
||||||
|
- 作用于**该 2D 类型自己那块平面底图**(§9.2,由 `Plane2DRenderStrategy` 持有)。
|
||||||
|
|
||||||
|
### 7.5 3D 底图(移到 `VtkViewToolbar`,非段操作)
|
||||||
|
- 在 `Gear`(坐标轴设置) 正下方新增「地图」图标按钮,点击弹 popup:【底图类型:天地图(默认) / 无】+【透明度滑块(默认 50%)】。
|
||||||
|
- 控制**全局唯一**的 3D 底图(`TileBasemap`):「无」= 隐藏;透明度去掉固定 `0.55`、改默认 `0.5` 可调。
|
||||||
|
|
||||||
|
### 7.6 导入雷达(移到 `TopBar` 设备菜单,临时测试)
|
||||||
|
- `TopBar` 设备菜单加「导入雷达测线」→ 子项「规范化(.head/.data)…」「Impulse(.iprb)…」,发等价于现 `radarImportRequested(impulse)` 的信号到既有导入流程。
|
||||||
|
- 从 `CategorySection`(voxel 段头) 移除该入口及 `radarImportRequested` 转发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 渲染模型(经渲染策略 §5.4)
|
||||||
|
|
||||||
|
> 控制器主流程:勾选 diff → 对每个新增/移除 ds 查其描述符 → 取 `renderStrategyId` 对应策略 → `add/remove`;某类型「首勾」「全消」时调 `onTypeActivated/Deactivated`。无维度/ddCode 分支。
|
||||||
|
|
||||||
|
### 8.1 删除维度耦合
|
||||||
|
- 移除 `VtkSceneView::setAnalysisMode2D` 的「相机锁定俯视 + 按维度翻可见标志」与 `VtkViewToolbar::setAnalysisMode2D` 的 6 向禁用。场景恒自由透视;2D/3D actor 同时可见(各自由勾选控制显隐)。
|
||||||
|
- 移除 `view2DMode` 5 模式与旧 `set2DPlacement` mode 维度(其职责并入 `Plane2DRenderStrategy`)。
|
||||||
|
|
||||||
|
### 8.2 2D 按类型平面(需求 5)—— 封装在 `Plane2DRenderStrategy`
|
||||||
|
- 同一 2D 类型(段)勾选的全部 ds 投影到**一块平面**:
|
||||||
|
- 平面 z = 该类型**第一个被勾选 ds** 的 z(首个勾选时确定后**固定不变**,仅由 z 滑块整体升降)。
|
||||||
|
- 平面**纯平、不渲染高程**。
|
||||||
|
- 同类型其余 ds 的折线全部落在此平面 z。
|
||||||
|
- **平面生命周期**:该类型**首个 ds 被勾选** → 创建平面(定 z)+ 创建其平面底图(§9.2);期间 z 固定;该类型**全部 ds 取消勾选** → 平面**与其底图一并销毁**。
|
||||||
|
- 逐 ds 独立拖动 Z(旧 `nudgeSelectedMapLinesZ` / `mapLineZOffset_`)**废弃**,统一到类型平面 z。
|
||||||
|
- 渲染复用 `MapLineActor`(折线几何不变),仅 Z 落点改为「所属类型平面 z」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 底图体系(§6)
|
||||||
|
|
||||||
|
### 9.1 3D 共享底图(1 个)
|
||||||
|
- 沿用 `TileBasemap` 单例(带高程地形,Z=0)。透明度参数化(默认 0.5,由 §7.5 popup 调);支持隐藏。瓦片范围规则不变。
|
||||||
|
|
||||||
|
### 9.2 2D 平面底图(N 个,每类型一块)—— 实现选型已定
|
||||||
|
- **决策:参数化 `TileBasemap` 支持多实例**(不另抽 `PlaneBasemap`)。依据:`TileBasemap` 的「相机驱动 LOD + 四叉树细分 + 视锥剔除 + 限并发下载 + GeoLocalFrame 配准」正是平面底图所需,且其状态全为 per-instance(无全局/静态状态,`tileKey` 为纯函数),多实例天然可行。新抽类要么重写数百行 LOD/网络逻辑,要么退化成单层无 LOD 大平面(缩放发虚、且违背「瓦片范围参考三维规则」)。
|
||||||
|
- **需改的 3 处硬编码**:
|
||||||
|
| 改动 | 现状 | 改为 |
|
||||||
|
|---|---|---|
|
||||||
|
| 地面 Z | `kGroundZ=0` 常量 | 构造/setter 传 `groundZ`(2D 平面 = 类型平面 z) |
|
||||||
|
| 透明度 | `kTerrainOpacity=0.55` 固定 | 参数化,默认 0.5 可调 |
|
||||||
|
| 平面/矢量模式 | 由 `Kind` 隐含 | 复用 `Street`(vec_w)+`buildFlat`(已是纯平矢量路径,跳过 DEM/warp) |
|
||||||
|
- **最终布局**:1 个 `TileBasemap`(3D,Satellite/带高程,Z=0,§9.1,挂控制器)+ N 个 `TileBasemap`(每 2D 类型一个,Street/纯平矢量,`groundZ`=平面 z,**由 `Plane2DRenderStrategy` 持有**)。共享同一 `Scene` / `GeoLocalFrame`。
|
||||||
|
- **生命周期**(在 `Plane2DRenderStrategy` 内,经 `onTypeActivated/Deactivated`):按 2D 类型持有 N 个实例 + 各自平面 z(`PlaneZRegistry`);该类型首勾 → 建实例(§8.2 定 z);该类型全消 → 销毁实例(连同折线平面)。
|
||||||
|
- 瓦片范围**复用** `dataHorizontalRadius()×10` 钳 `[2000,30000]m` 规则(各实例共享同一 `dataRadiusProvider`)。
|
||||||
|
- 坐标对齐沿用 `GeoLocalFrame`(经纬→局部);frame 重锚逻辑不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 信号 / 默认勾选(§7)
|
||||||
|
|
||||||
|
- 2D 勾选/选中信号从 `Column2DDataset` 迁到 2D `CategorySection`(复用 `checkedDatasetsChanged` / `datasetSelected`)。`basemapChanged` / `view2DModeChanged` / `customZChanged` 退役,由 §7.4/§7.5 的 popup + §8.2 平面 z 替代。
|
||||||
|
- 「直接挂项目下 ds 默认进 VTK」既有逻辑保留;动态显隐段时确保默认勾选的 ds 所属段被显示且勾选状态正确。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 迁移说明 / 取舍 / 待定
|
||||||
|
|
||||||
|
- **替代 2026-06-26 spec**:该 spec 的「一场景两相机 + 按维度显隐 + 高程拖动分层」中,**相机锁定 + 维度显隐被本 spec 推翻**(改单一自由场景共存)。其「2D 沿 Z 拖动分离」语义改由「按类型平面 + z 滑块」承担——逐 ds 独立拖动 `nudgeSelectedMapLinesZ` / `mapLineZOffset_`(及拾取拖动浮层读数)**废弃移除**,统一到类型平面 z。
|
||||||
|
- **`dd_raster`** 仍未接(2026-06-26 §6 遗留),本 spec 不含;后续作为新的 2D 类型段加入时复用 §8.2/§9.2 平面+底图机制。
|
||||||
|
- **3D 反演段 vs 三维体段**:均为 D3,但 3D 反演段渲染为帘面、三维体段为体素/雷达体——共用 3D 底图与自由场景,无需区分。
|
||||||
|
- **责任拆分**:`CategoryDescriptor`/`categoryCatalog`(描述符)、`IDatasetRenderStrategy` 注册表(渲染)、`SectionIconBar`(响应式工具条)、`TileBasemap` 多实例、`DatasetColumn`(动态显隐)相互独立,可分别实现与测试。
|
||||||
|
- **抽象的 YAGNI 边界**:`FilterKind`/`OpKind`/`SceneKind` 只列当前真用到的枚举值;渲染策略只实现 `volume`/`curtain`/`plane2d` 三个。扩展点是「加新值/新策略」的预留口,不预先实现任何未用类型。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 验收
|
||||||
|
|
||||||
|
1. 左侧只有**一个无标题数据集栏**(无 tab);其中同时出现 3D 类型段(电阻率/视电阻率/瞬变/三维体)与 2D 类型段(轨迹)。
|
||||||
|
2. 段**动态显隐**:无对应数据的段不显示;面板全空时显示居中占位提示。
|
||||||
|
3. 段头操作为**图标工具条**:默认最多 3 个,超出/挤压时右侧收进「…」下拉。3D 反演段含「新增三维体/筛选」,2D 轨迹段含「z值/筛选/底图」。
|
||||||
|
4. 「筛选」图标可**展开/收起**段内筛选行(默认折叠)。
|
||||||
|
5. 勾选 2D 轨迹:同类型 ds 投影到**一块纯平平面**,平面 z = 首个勾选 ds 的 z(之后固定);「z值」滑块整体升降该平面;「底图」popup 可换矢量平面底图/无 + 调透明度(默认 50%)。该类型**全部取消勾选 → 平面与其底图一并消失**。
|
||||||
|
6. 3D 与 2D 数据在**同一自由透视场景**同时可见,可自由旋转(无锁定俯视、无 tab 切换)。
|
||||||
|
7. 渲染区工具栏 Gear 下方新增「地图」按钮,控制全局 3D 底图(天地图/无 + 透明度默认 50%)。
|
||||||
|
8. 「导入雷达」入口出现在顶部「设备」菜单;三维体段头不再有该按钮。
|
||||||
|
9. 「直接挂项目下的 ds」加载时默认勾选进 VTK 不被破坏。
|
||||||
|
10. **可扩展性**:现存 5 类全部经 `categoryCatalog()` 描述符 + 渲染策略注册表驱动,控制器/段头无维度/ddCode 散判。新增一个「沿用已有渲染/操作/筛选」的类型,只需在 catalog 加一条描述符即可显示+渲染(以一个验证性 demo 描述符或单元测试佐证分类/路由走通)。
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
# VTK 视图导航与坐标轴改进 — Spec(2026-07-01)
|
||||||
|
|
||||||
|
> 分支 `feat/vtk-merged-dataset-column`(合并数据集单栏重构之后的增量)。职责:渲染区坐标轴、方向标、相机导航、列表双击联动。全部决策已与用户逐条确认。
|
||||||
|
|
||||||
|
## 0. 背景与动机
|
||||||
|
|
||||||
|
合并单栏 + 单一自由场景后,2D 平面与 3D 体/帘面共存于一个世界坐标系。现状渲染坐标轴是**全场景合并包围盒**的一套 cube axes(`AxesActor`/`rebuildAxes`)。用户痛点:多个渲染物相距很远/尺度悬殊时,全场景轴只贴近某一个物体,**离轴远的物体读不出真实尺寸**。业界通行解法不是「每物体一套常驻轴」,而是「全场景一套参考 + 选中物体单独出其贴合轴 + 可点击方向标 + 按需测量」。本 spec 落地导航侧改进。
|
||||||
|
|
||||||
|
## 1. 已确认决策(逐条)
|
||||||
|
|
||||||
|
1. **坐标轴(空间)全场景一套是对的**——坐标轴量的是空间位置(一个世界 CS),物理量差异由逐数据集色阶承担,尺度悬殊用统一 VE,不拆轴。
|
||||||
|
2. **选中物体 → 隐全景轴、只显该物体贴合轴**(Q1)。
|
||||||
|
3. **贴合轴按层级子树归一到一个包围盒**:选中三维体(或其下切片、切片下异常)→ 包围盒覆盖「该三维体 + 其切片 + 其异常」整棵子树,合成**一个**盒;选谁都归到该子树同一个盒。
|
||||||
|
4. **角落三向标(gnomon)常驻 + 可点击**(Q2);点击某方向轴 → 相机**绕支点转到该轴、保留当前缩放距离**(ViewCube 手感,选项1)。
|
||||||
|
5. **绕轴支点规则**:有选中 → 选中物体**子树包围盒中心**;无选中 → **全场景合并包围盒中心**。两者都保留当前缩放。
|
||||||
|
6. **列表双击 DS →(a)视图适配到该 DS 的空间范围 +(b)联动打开中下方数据集详情页**;三维体等**详情页未做**的类型要**容错静默**(找不到对应详情页不报错、不弹窗,仅完成适配)。
|
||||||
|
|
||||||
|
## 2. 术语:子树包围盒
|
||||||
|
|
||||||
|
- 一个 ds 的「子树」= 该 ds 自身 + 其所有后代 ds(三维体 → 其切片 → 切片的异常),仅计入**当前已渲染**的成员。
|
||||||
|
- 子树包围盒 = 子树内所有已渲染 ds 的 actor 包围盒并集(`dsProps_` 按 dsId 取 actor bounds)。
|
||||||
|
- 后代关系来源:数据模型的父子(切片 parentId=体、异常 parentId=切片/体)——由控制器/仓储或面板树提供 dsId→后代集。渲染侧只需拿到「要并集的 dsId 列表」。
|
||||||
|
|
||||||
|
## 3. 设计
|
||||||
|
|
||||||
|
### 3.1 相机/包围盒基元(`VtkSceneView` + `CameraPreset`)
|
||||||
|
- `bool datasetBounds(const std::vector<std::string>& dsIds, double outB[6]) const`:并集给定 dsIds 的已渲染 actor 包围盒;无有效则返回 false。
|
||||||
|
- `void fitToBounds(const double b[6])`:把相机适配到指定包围盒(保持朝向,`ResetCamera(b)`)。用于双击适配、贴合。
|
||||||
|
- `void orbitToAxis(ViewDir dir, const double pivot[3])`:相机**绕 pivot** 转到沿 dir 轴看向 pivot,**保留当前 focal-to-camera 距离**(即当前缩放)。区别于 `applyCameraView`(正视重置)与 6 视图按钮(重置+fit)。实现:取当前 |cam−focal| 距离 d;focal 设为 pivot;按 dir 设 cam=pivot+dir_offset*d、view-up 按预设;`ResetCameraClippingRange`。
|
||||||
|
|
||||||
|
### 3.2 贴合轴 + 隐全景轴(决策 2/3)
|
||||||
|
- `VtkSceneView` 维护两态:`rebuildAxes()`(全场景,现状)与新 `showFittedAxes(const double b[6])`/`showSceneAxes()`。
|
||||||
|
- 选中某 ds(`datasetSelected` 到达)→ 上层算该 ds 子树的 dsIds → `datasetBounds` → `showFittedAxes(box)`(把 cube axes 的 bounds 设为子树盒)+ 隐全景轴。
|
||||||
|
- 取消选中(空选中)→ `showSceneAxes()`(恢复全场景轴)。
|
||||||
|
- 实现可复用 `AxesActor`:新增「按外部 bounds 显示」的模式(不再固定用全场景 bounds),选中/取消在两种 bounds 间切换。
|
||||||
|
|
||||||
|
### 3.3 可点击方向标 gnomon(决策 4/5)
|
||||||
|
- 新增角落三向标:`vtkOrientationMarkerWidget` + `vtkAxesActor`(XYZ 三向 + 轴标签),常驻右下(或右上,避开现有工具栏)。
|
||||||
|
- **可点击**:`vtkOrientationMarkerWidget` 默认不透传点击到轴。需在其上做拾取——用一个 `vtkPropPicker`/自定义,把点击落到 +X/−X/+Y/−Y/+Z/−Z 六向之一 → 调 `orbitToAxis(dir, pivot)`。pivot 按决策 5(选中子树中心 / 全场景中心)。
|
||||||
|
- 六向 → `ViewDir` 映射:+Z=Top、−Z=Bottom、+Y=Back(北望反)/−Y=Front… 与 `CameraPreset` 现有 ViewDir 语义对齐(复用,不新造方向定义)。
|
||||||
|
|
||||||
|
### 3.4 双击 DS → 适配 + 详情(决策 6)
|
||||||
|
- 列表双击现已发 `detailRequested(dsId, ddCode, name)`(`CategorySection::itemDoubleClicked`)。扩展双击行为:**并发**触发
|
||||||
|
1. **适配**:算该 dsId 子树 dsIds → `datasetBounds` → `fitToBounds`(相机适配到该 DS 空间范围,保持朝向)。
|
||||||
|
2. **详情联动**:沿用现有 `detailRequested → DatasetDetailController` 链打开中下方 `DatasetDetailPanel`。
|
||||||
|
- **容错静默**:对详情页未实现的类型(三维体 `dd_voxel`/`dd_radar_3d` 等),详情链找不到对应页时**静默**——不弹错、不打断,适配照常完成。实现:详情控制器/面板在无匹配详情页时走「无操作」分支(或双击前按 ddCode 判定「有详情页才发 detailRequested」)。优先在联动入口按「该 ddCode 是否有详情页」gate,无则只做适配。
|
||||||
|
|
||||||
|
## 4. 影响文件(预估)
|
||||||
|
- `src/app/VtkSceneView.{hpp,cpp}`:`datasetBounds`/`fitToBounds`/`orbitToAxis`/`showFittedAxes`/`showSceneAxes` + gnomon widget。
|
||||||
|
- `src/render/CameraPreset.{hpp,cpp}` 或 `src/render/actors/AxesActor.*`:orbit-to-axis 相机数学、按外部 bounds 的轴。
|
||||||
|
- `src/controller/VtkSceneController.*` + `I3dSceneView`:转发新相机/轴/适配接口;子树 dsIds 解析(或由 main 用面板树/仓储父子算)。
|
||||||
|
- `src/app/main.cpp`:选中→贴合轴、双击→适配+详情 gate、gnomon 点击接线。
|
||||||
|
- `src/app/panels/columns/CategorySection.*`/`CategoryAnalysisTab.*`:双击透传(已有 detailRequested;可能加「fitRequested(dsId)」或复用 datasetSelected)。
|
||||||
|
|
||||||
|
## 5. 分期/任务
|
||||||
|
- **T1(基元)**:`datasetBounds`/`fitToBounds`/`orbitToAxis`(相机+包围盒基元,纯渲染,可单测 orbit 数学)。
|
||||||
|
- **T2(贴合轴)**:选中→子树盒→贴合轴+隐全景轴;取消→恢复全景轴。含「子树 dsIds 解析」。
|
||||||
|
- **T3(gnomon)**:常驻可点击三向标 → orbitToAxis(pivot 规则)。
|
||||||
|
- **T4(双击)**:双击→适配到子树盒 + 详情联动 gate(无详情页静默)。
|
||||||
|
- 顺序:T1 先(T2/T3/T4 都依赖 T1 的基元);T2/T4 依赖「子树 dsIds 解析」(T2 引入,T4 复用)。
|
||||||
|
|
||||||
|
## 6. 验收
|
||||||
|
1. 无选中:显示全场景总览轴 + 右下角三向标。
|
||||||
|
2. 选中某体/切片/异常:全景轴隐去,出现覆盖「该体+其切片+异常」子树的一个贴合包围盒轴,能读该子树真实尺寸;取消选中恢复全景轴。
|
||||||
|
3. 点击三向标某方向轴:相机绕支点(选中子树中心/无选中则全场景中心)转到该轴、**缩放不变**、数据仍居中。
|
||||||
|
4. 双击列表某 DS:视图适配到该 DS(子树)空间范围;有详情页的类型联动打开中下方详情页;三维体等无详情页类型**静默**(只适配、不报错)。
|
||||||
|
5. VE 对全部渲染物一致;坐标轴仍是空间位置度量、逐数据集色阶不变。
|
||||||
|
|
@ -5,8 +5,10 @@ find_package(VTK REQUIRED COMPONENTS
|
||||||
GUISupportQt
|
GUISupportQt
|
||||||
RenderingOpenGL2
|
RenderingOpenGL2
|
||||||
RenderingVolumeOpenGL2
|
RenderingVolumeOpenGL2
|
||||||
|
RenderingAnnotation # vtkAxesActor(角落方向标 gnomon 三向箭头 + 轴标签)
|
||||||
InteractionStyle
|
InteractionStyle
|
||||||
InteractionWidgets
|
InteractionWidgets
|
||||||
|
FiltersSources # vtkSphereSource(gnomon 6 向可点击方向球)
|
||||||
FiltersGeometry
|
FiltersGeometry
|
||||||
FiltersModeling
|
FiltersModeling
|
||||||
IOImage # vtkPNGWriter(切片导出图片)
|
IOImage # vtkPNGWriter(切片导出图片)
|
||||||
|
|
@ -80,11 +82,11 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/chart/ChartPickGeometry.cpp
|
panels/chart/ChartPickGeometry.cpp
|
||||||
panels/chart/ScatterMarqueePicker.cpp
|
panels/chart/ScatterMarqueePicker.cpp
|
||||||
panels/chart/ContourDrawTool.cpp
|
panels/chart/ContourDrawTool.cpp
|
||||||
panels/columns/Column2DDataset.cpp
|
|
||||||
panels/columns/CategorySection.cpp
|
panels/columns/CategorySection.cpp
|
||||||
panels/columns/CategoryAnalysisTab.cpp
|
panels/columns/CategoryAnalysisTab.cpp
|
||||||
panels/columns/DateRangeEdit.cpp
|
panels/columns/DateRangeEdit.cpp
|
||||||
panels/columns/ColumnDrawer.cpp
|
panels/columns/ColumnDrawer.cpp
|
||||||
|
panels/columns/SectionIconBar.cpp
|
||||||
panels/AnomalyTablePanel.cpp
|
panels/AnomalyTablePanel.cpp
|
||||||
panels/LoadingOverlay.cpp
|
panels/LoadingOverlay.cpp
|
||||||
panels/DatasetDetailPage.cpp
|
panels/DatasetDetailPage.cpp
|
||||||
|
|
@ -109,7 +111,6 @@ add_executable(geopro_desktop WIN32
|
||||||
VolumeParamsDialog.cpp
|
VolumeParamsDialog.cpp
|
||||||
VolumePropertiesDialog.cpp
|
VolumePropertiesDialog.cpp
|
||||||
Logging.cpp
|
Logging.cpp
|
||||||
DatasetDimension.cpp
|
|
||||||
DatasetCategory.cpp
|
DatasetCategory.cpp
|
||||||
VtkViewToolbar.cpp
|
VtkViewToolbar.cpp
|
||||||
AxesSettingsDialog.cpp
|
AxesSettingsDialog.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
#include "DatasetCategory.hpp"
|
#include "DatasetCategory.hpp"
|
||||||
|
#include "repo/CategoryDescriptor.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows) {
|
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows) {
|
||||||
const auto& cfg = categoryConfigs();
|
const auto& cat = geopro::data::categoryCatalog();
|
||||||
CategoryBuckets b;
|
CategoryBuckets b;
|
||||||
b.segments.resize(cfg.size());
|
b.segments.resize(cat.size());
|
||||||
for (const auto& r : rows) {
|
for (const auto& r : rows)
|
||||||
int hit = -1;
|
for (std::size_t i = 0; i < cat.size(); ++i)
|
||||||
// 先按 ddCode(三维体/切片)——它们无 dsTypeCode(来自 Api3dRepository mock 行)。
|
if (cat[i].classify && cat[i].classify(r)) { b.segments[i].push_back(r); break; }
|
||||||
for (std::size_t i = 0; i < cfg.size() && hit < 0; ++i)
|
|
||||||
if (!cfg[i].ddCode.empty() && r.ddCode == cfg[i].ddCode) hit = static_cast<int>(i);
|
|
||||||
// 再按 dsTypeCode。
|
|
||||||
for (std::size_t i = 0; i < cfg.size() && hit < 0; ++i)
|
|
||||||
if (!cfg[i].dsTypeCode.empty() && r.dsTypeCode == cfg[i].dsTypeCode) hit = static_cast<int>(i);
|
|
||||||
if (hit >= 0) b.segments[static_cast<std::size_t>(hit)].push_back(r);
|
|
||||||
}
|
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "repo/CategoryConfig.hpp"
|
|
||||||
#include "repo/RepoTypes.hpp"
|
#include "repo/RepoTypes.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
struct CategoryBuckets {
|
struct CategoryBuckets {
|
||||||
std::vector<std::vector<geopro::data::DsRow>> segments; // 与 categoryConfigs() 同序同长
|
std::vector<std::vector<geopro::data::DsRow>> segments; // 与 categoryCatalog() 同序同长
|
||||||
};
|
};
|
||||||
|
|
||||||
// 按 CategoryConfig 把 ds 分入大类段:先判 ddCode 白名单(三维体/切片),否则按 dsTypeCode 匹配;
|
// 按 categoryCatalog() 把 ds 分入大类段:遍历目录,命中首个 classify(row)==true 的描述符即归入该段;
|
||||||
// 不在表内的丢弃(接地电阻/原始数据/白化/坐标等)。保留原顺序。
|
// 不在表内的丢弃(接地电阻/原始数据/白化/坐标等)。保留原顺序。
|
||||||
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows);
|
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
#include "DatasetDimension.hpp"
|
|
||||||
|
|
||||||
namespace geopro::app {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// 与 LocalSample3dRepository::dimensionOf 同一映射(spec §6.1)。
|
|
||||||
enum class Dim { D3, D2, Analysis, Other };
|
|
||||||
Dim dimOf(const std::string& c) {
|
|
||||||
if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" ||
|
|
||||||
c == "dd_section" || c == "dd_inversion_data" || c == "dd_radar_3d")
|
|
||||||
return Dim::D3;
|
|
||||||
if (c == "dd_slice") return Dim::Analysis;
|
|
||||||
if (c == "dd_trajectory_data") return Dim::D2;
|
|
||||||
return Dim::Other;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
DimBuckets splitByDimension(const std::vector<geopro::data::DsRow>& rows) {
|
|
||||||
DimBuckets b;
|
|
||||||
for (const auto& r : rows) {
|
|
||||||
switch (dimOf(r.ddCode)) {
|
|
||||||
case Dim::D3: b.dim3D.push_back(r); break;
|
|
||||||
case Dim::D2: b.dim2D.push_back(r); break;
|
|
||||||
case Dim::Analysis: b.analysis.push_back(r); break;
|
|
||||||
case Dim::Other: break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace geopro::app
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <vector>
|
|
||||||
#include "repo/RepoTypes.hpp"
|
|
||||||
|
|
||||||
namespace geopro::app {
|
|
||||||
|
|
||||||
struct DimBuckets {
|
|
||||||
std::vector<geopro::data::DsRow> dim3D;
|
|
||||||
std::vector<geopro::data::DsRow> dim2D;
|
|
||||||
std::vector<geopro::data::DsRow> analysis;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 按 ddCode 把 ds 分流到 三维数据集 / 二维数据集 / 三维分析 三栏。
|
|
||||||
// Other 维度不入任何栏(保留原顺序)。
|
|
||||||
DimBuckets splitByDimension(const std::vector<geopro::data::DsRow>& rows);
|
|
||||||
|
|
||||||
} // namespace geopro::app
|
|
||||||
|
|
@ -52,11 +52,9 @@ constexpr double kRangeCeil = 30000.0; // 最多 30km(防远裁剪面失控)
|
||||||
constexpr int kMaxConcurrent = 12; // 瓦片请求最大并发(天地图 8 子域+Mapbox,适度提高吞吐)
|
constexpr int kMaxConcurrent = 12; // 瓦片请求最大并发(天地图 8 子域+Mapbox,适度提高吞吐)
|
||||||
constexpr int kMinZoom = 3;
|
constexpr int kMinZoom = 3;
|
||||||
constexpr int kMaxZoom = 18;
|
constexpr int kMaxZoom = 18;
|
||||||
constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面深度向下为负,落其下)
|
|
||||||
constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting
|
constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting
|
||||||
constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存
|
constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存
|
||||||
constexpr double kPi = 3.14159265358979323846;
|
constexpr double kPi = 3.14159265358979323846;
|
||||||
constexpr double kTerrainOpacity = 0.55; // 地形半透明:地下剖面可从任意角度透过地面看到(不再被遮挡)
|
|
||||||
|
|
||||||
// 地面起伏:Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN,比 AWS Terrarium 快)。
|
// 地面起伏:Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN,比 AWS Terrarium 快)。
|
||||||
// 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15,更高层级取祖先块。
|
// 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15,更高层级取祖先块。
|
||||||
|
|
@ -121,8 +119,9 @@ long long TileBasemap::tileKey(int z, int x, int y) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
||||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent)
|
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent,
|
||||||
: QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)) {}
|
double groundZ)
|
||||||
|
: QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)), groundZ_(groundZ) {}
|
||||||
|
|
||||||
void TileBasemap::requestRender() {
|
void TileBasemap::requestRender() {
|
||||||
// 合并渲染:同一事件循环轮次内的多次请求只渲染一帧(标准做法,避免逐瓦片重复 Render 卡顿)。
|
// 合并渲染:同一事件循环轮次内的多次请求只渲染一帧(标准做法,避免逐瓦片重复 Render 卡顿)。
|
||||||
|
|
@ -140,6 +139,10 @@ void TileBasemap::requestRender() {
|
||||||
}
|
}
|
||||||
|
|
||||||
TileBasemap::~TileBasemap() {
|
TileBasemap::~TileBasemap() {
|
||||||
|
// 移除本实例所有已贴瓦片:多实例(每 2D 平面一份)动态建销时,析构须撤回瓦片,否则渲染器仍持引用、
|
||||||
|
// 底图不随平面消失。共享 3D 底图存活至退出故旧码无此清理也无碍,但 per-plane 实例必须清。
|
||||||
|
if (auto* ren = scene_.renderer())
|
||||||
|
for (auto& kv : placed_) ren->RemoveViewProp(kv.second);
|
||||||
if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_);
|
if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,6 +211,23 @@ void TileBasemap::setVerticalExaggeration(double ve) {
|
||||||
if (kind_ != Hidden) show(kind_); // 重建地形(高程×新VE),与剖面 VE 保持一致
|
if (kind_ != Hidden) show(kind_); // 重建地形(高程×新VE),与剖面 VE 保持一致
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TileBasemap::setOpacity(double o) {
|
||||||
|
o = std::clamp(o, 0.0, 1.0);
|
||||||
|
if (o == opacity_) return;
|
||||||
|
opacity_ = o;
|
||||||
|
if (kind_ != Hidden) show(kind_); // 重建套用新透明度
|
||||||
|
}
|
||||||
|
|
||||||
|
void TileBasemap::setGroundZ(double z) {
|
||||||
|
// 直接平移平面高程:瓦片几何建于相对 z(仅含逐层级 z-fighting 偏移),平面高程 groundZ_ 经 actor position 施加。
|
||||||
|
// 拖 z 值滑块时只改所有已贴瓦片的 SetPosition,无需重下载/重建;后续 refresh() 新瓦片经 placeActor 自动取新 groundZ_。
|
||||||
|
if (z == groundZ_) return;
|
||||||
|
groundZ_ = z;
|
||||||
|
for (auto& kv : placed_)
|
||||||
|
if (kv.second) kv.second->SetPosition(0.0, 0.0, groundZ_);
|
||||||
|
requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int& count) {
|
void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int& count) {
|
||||||
if (count >= kMaxLeaves) { out.insert(tileKey(z, x, y)); return; } // 安全上限:停止细分
|
if (count >= kMaxLeaves) { out.insert(tileKey(z, x, y)); return; } // 安全上限:停止细分
|
||||||
const int n = 1 << z;
|
const int n = 1 << z;
|
||||||
|
|
@ -429,6 +449,7 @@ void TileBasemap::fetchTile(int z, int x, int y, long long key) {
|
||||||
|
|
||||||
void TileBasemap::placeActor(long long key, vtkSmartPointer<vtkActor> actor) {
|
void TileBasemap::placeActor(long long key, vtkSmartPointer<vtkActor> actor) {
|
||||||
if (!actor) return;
|
if (!actor) return;
|
||||||
|
actor->SetPosition(0.0, 0.0, groundZ_); // 平面高程经 position 施加:几何建于相对 z,此处抬到当前 groundZ_
|
||||||
scene_.addActor(actor);
|
scene_.addActor(actor);
|
||||||
placed_[key] = actor;
|
placed_[key] = actor;
|
||||||
}
|
}
|
||||||
|
|
@ -439,7 +460,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
|
||||||
const auto sw = frame_->toLocal(b.south, b.west);
|
const auto sw = frame_->toLocal(b.south, b.west);
|
||||||
const auto se = frame_->toLocal(b.south, b.east);
|
const auto se = frame_->toLocal(b.south, b.east);
|
||||||
const auto nw = frame_->toLocal(b.north, b.west);
|
const auto nw = frame_->toLocal(b.north, b.west);
|
||||||
const double gz = kGroundZ + (z - kMinZoom) * kZEps; // 高层级略抬高,压在旧层之上防共面闪烁
|
const double gz = (z - kMinZoom) * kZEps; // 仅逐层级 z-fighting 偏移(相对 z);平面高程由 actor position 施加
|
||||||
|
|
||||||
// PlaneSource 自动 tcoord:origin=SW→u 西0东1、v 南0北1(与翻转后纹理对齐)。
|
// PlaneSource 自动 tcoord:origin=SW→u 西0东1、v 南0北1(与翻转后纹理对齐)。
|
||||||
vtkNew<vtkPlaneSource> plane;
|
vtkNew<vtkPlaneSource> plane;
|
||||||
|
|
@ -452,7 +473,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
|
||||||
actor->SetMapper(mapper);
|
actor->SetMapper(mapper);
|
||||||
actor->SetTexture(tex);
|
actor->SetTexture(tex);
|
||||||
actor->GetProperty()->LightingOff(); // 底图不受场景光照
|
actor->GetProperty()->LightingOff(); // 底图不受场景光照
|
||||||
actor->GetProperty()->SetOpacity(kTerrainOpacity); // 半透明:不遮挡地下剖面
|
actor->GetProperty()->SetOpacity(opacity_); // 半透明:不遮挡地下剖面
|
||||||
// 注意:UseBounds 保持默认 true → 参与相机裁剪面计算,否则底图会被裁剪面"蒙版"切掉。
|
// 注意:UseBounds 保持默认 true → 参与相机裁剪面计算,否则底图会被裁剪面"蒙版"切掉。
|
||||||
// 坐标轴/取景不被底图撑大,由 VtkSceneView 改用"数据自身包围盒"解决(非靠 UseBounds=false)。
|
// 坐标轴/取景不被底图撑大,由 VtkSceneView 改用"数据自身包围盒"解决(非靠 UseBounds=false)。
|
||||||
return actor;
|
return actor;
|
||||||
|
|
@ -528,7 +549,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int d
|
||||||
const auto sw = frame_->toLocal(sb.south, sb.west);
|
const auto sw = frame_->toLocal(sb.south, sb.west);
|
||||||
const auto se = frame_->toLocal(sb.south, sb.east);
|
const auto se = frame_->toLocal(sb.south, sb.east);
|
||||||
const auto nw = frame_->toLocal(sb.north, sb.west);
|
const auto nw = frame_->toLocal(sb.north, sb.west);
|
||||||
const double base = kGroundZ + (sz - kMinZoom) * kZEps;
|
const double base = (sz - kMinZoom) * kZEps; // 仅逐层级 z-fighting 偏移(相对 z);平面高程由 actor position 施加
|
||||||
|
|
||||||
// PlaneSource(等距圆柱下平面插值即正确 x/y) + 自动 tcoord;再按各点真实经纬采 DEM 位移 Z。
|
// PlaneSource(等距圆柱下平面插值即正确 x/y) + 自动 tcoord;再按各点真实经纬采 DEM 位移 Z。
|
||||||
vtkNew<vtkPlaneSource> plane;
|
vtkNew<vtkPlaneSource> plane;
|
||||||
|
|
@ -566,7 +587,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int d
|
||||||
actor->SetMapper(mapper);
|
actor->SetMapper(mapper);
|
||||||
actor->SetTexture(tex);
|
actor->SetTexture(tex);
|
||||||
actor->GetProperty()->LightingOff();
|
actor->GetProperty()->LightingOff();
|
||||||
actor->GetProperty()->SetOpacity(kTerrainOpacity); // 半透明:不遮挡地下剖面
|
actor->GetProperty()->SetOpacity(opacity_); // 半透明:不遮挡地下剖面
|
||||||
return actor; // UseBounds 默认 true:参与裁剪面,避免被"蒙版"切掉
|
return actor; // UseBounds 默认 true:参与裁剪面,避免被"蒙版"切掉
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,16 @@ class TileBasemap : public QObject {
|
||||||
public:
|
public:
|
||||||
enum Kind { Street = 0, Satellite = 1, Hidden = 2 };
|
enum Kind { Street = 0, Satellite = 1, Hidden = 2 };
|
||||||
TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
||||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr);
|
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr,
|
||||||
|
double groundZ = 0.0);
|
||||||
~TileBasemap() override;
|
~TileBasemap() override;
|
||||||
|
|
||||||
void show(Kind kind); // 显示某底图(Hidden 等同 hide);记住类型供 LOD 刷新复用
|
void show(Kind kind); // 显示某底图(Hidden 等同 hide);记住类型供 LOD 刷新复用
|
||||||
void hide(); // 移除全部瓦片
|
void hide(); // 移除全部瓦片
|
||||||
void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调)
|
void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调)
|
||||||
void setVerticalExaggeration(double ve); // 地形垂向夸张(须与剖面 VE 一致才对齐)
|
void setVerticalExaggeration(double ve); // 地形垂向夸张(须与剖面 VE 一致才对齐)
|
||||||
|
void setOpacity(double o); // 底图半透明度[0,1],供渲染工具栏底图弹窗调节
|
||||||
|
void setGroundZ(double z); // 直接平移底图平面 z(拖 z 值滑块):改所有已贴瓦片 actor 的 position,无重铺
|
||||||
// 数据半径提供者:刷新时查询当前所有勾选剖面的合并范围(半径,米),据此动态定底图最大范围。
|
// 数据半径提供者:刷新时查询当前所有勾选剖面的合并范围(半径,米),据此动态定底图最大范围。
|
||||||
void setDataRadiusProvider(std::function<double()> fn) { dataRadiusProvider_ = std::move(fn); }
|
void setDataRadiusProvider(std::function<double()> fn) { dataRadiusProvider_ = std::move(fn); }
|
||||||
|
|
||||||
|
|
@ -75,7 +78,9 @@ private:
|
||||||
std::set<long long> inFlight_; // 在途瓦片(续到起伏/平面最终落地)
|
std::set<long long> inFlight_; // 在途瓦片(续到起伏/平面最终落地)
|
||||||
std::map<long long, QImage> demCache_; // DEM 块缓存(key=DEMz/x/y),跨隐藏/重选复用
|
std::map<long long, QImage> demCache_; // DEM 块缓存(key=DEMz/x/y),跨隐藏/重选复用
|
||||||
std::map<long long, vtkSmartPointer<vtkTexture>> texCache_; // 影像纹理缓存,重选/缩放回看免重拉
|
std::map<long long, vtkSmartPointer<vtkTexture>> texCache_; // 影像纹理缓存,重选/缩放回看免重拉
|
||||||
|
double groundZ_ = 0.0; // 底图地面参考 z(per-instance):3D 底图=0;2D 平面底图=各类型平面高程
|
||||||
double ve_ = 1.0; // 地形垂向夸张(与剖面 verticalExaggeration 一致才对齐)
|
double ve_ = 1.0; // 地形垂向夸张(与剖面 verticalExaggeration 一致才对齐)
|
||||||
|
double opacity_ = 0.5; // 底图半透明:地下剖面可从任意角度透过地面看到(不再被遮挡)
|
||||||
double maxTileDist_ = 2000.0; // 底图最大距离(米),每次刷新按剖面范围动态算
|
double maxTileDist_ = 2000.0; // 底图最大距离(米),每次刷新按剖面范围动态算
|
||||||
std::function<double()> dataRadiusProvider_; // 返回当前勾选剖面合并范围的半径
|
std::function<double()> dataRadiusProvider_; // 返回当前勾选剖面合并范围的半径
|
||||||
// 卫星层「学习到的」最大可用层级:天地图卫星影像各区域覆盖深度不同(内地到z18, 台湾等仅到z16),
|
// 卫星层「学习到的」最大可用层级:天地图卫星影像各区域覆盖深度不同(内地到z18, 台湾等仅到z16),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "TileBasemap.hpp"
|
||||||
|
#include "controller/DatasetRenderStrategy.hpp" // geopro::controller::IPlaneBasemap
|
||||||
|
|
||||||
|
namespace geopro::render { class Scene; }
|
||||||
|
namespace geopro::core { class GeoLocalFrame; }
|
||||||
|
class vtkRenderWindow;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 2D 平面底图适配器:把 app 层 TileBasemap 适配到控制器层抽象 IPlaneBasemap,
|
||||||
|
// 使 geopro_controller(仅链 geopro_data+Qt::Core) 不反依赖 app 层与 VTK(仿 I3dSceneView/VtkSceneView)。
|
||||||
|
// main.cpp 经底图工厂按平面 z 造之;持 TileBasemap 值成员,随适配器析构而析构(移除瓦片→底图随平面消失)。
|
||||||
|
class TileBasemapPlaneAdapter : public geopro::controller::IPlaneBasemap {
|
||||||
|
public:
|
||||||
|
TileBasemapPlaneAdapter(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
||||||
|
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double groundZ,
|
||||||
|
std::function<double()> radiusProvider)
|
||||||
|
: bm_(scene, rw, std::move(frame), nullptr, groundZ) {
|
||||||
|
bm_.setDataRadiusProvider(std::move(radiusProvider));
|
||||||
|
}
|
||||||
|
void show(int kind) override {
|
||||||
|
bm_.show(kind == 0 ? TileBasemap::Street : TileBasemap::Hidden);
|
||||||
|
}
|
||||||
|
void hide() override { bm_.hide(); }
|
||||||
|
void setOpacity(double o) override { bm_.setOpacity(o); }
|
||||||
|
void setGroundZ(double z) override { bm_.setGroundZ(z); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
TileBasemap bm_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -129,14 +129,6 @@ QMenu* buildToolsMenu(QWidget* p)
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMenu* buildDeviceMenu(QWidget* p)
|
|
||||||
{
|
|
||||||
auto* m = new QMenu(QStringLiteral("设备"), p);
|
|
||||||
m->addAction(QStringLiteral("连接设备"));
|
|
||||||
m->addAction(QStringLiteral("设备管理"));
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
|
|
@ -220,7 +212,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
lay->addWidget(makeMenuButton(this, buildViewMenu()));
|
lay->addWidget(makeMenuButton(this, buildViewMenu()));
|
||||||
lay->addWidget(makeMenuButton(this, buildProjectMenu()));
|
lay->addWidget(makeMenuButton(this, buildProjectMenu()));
|
||||||
lay->addWidget(makeMenuButton(this, buildToolsMenu(this)));
|
lay->addWidget(makeMenuButton(this, buildToolsMenu(this)));
|
||||||
lay->addWidget(makeMenuButton(this, buildDeviceMenu(this)));
|
lay->addWidget(makeMenuButton(this, buildDeviceMenu()));
|
||||||
|
|
||||||
lay->addStretch();
|
lay->addStretch();
|
||||||
|
|
||||||
|
|
@ -333,6 +325,21 @@ QMenu* TopBar::buildProjectMenu() {
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设备菜单。连接设备/设备管理为占位;「导入雷达测线」是后端未就绪期的过渡测试入口,集中到设备菜单。
|
||||||
|
// 原入口在三维体段头按钮(已移除);子项规范化/Impulse 分别 emit radarImportRequested(false/true),
|
||||||
|
// 由 main 接到既有导入流程。
|
||||||
|
QMenu* TopBar::buildDeviceMenu() {
|
||||||
|
auto* m = new QMenu(QStringLiteral("设备"), this);
|
||||||
|
m->addAction(QStringLiteral("连接设备"));
|
||||||
|
m->addAction(QStringLiteral("设备管理"));
|
||||||
|
QMenu* radar = m->addMenu(QStringLiteral("导入雷达测线"));
|
||||||
|
radar->addAction(QStringLiteral("规范化测线目录(.head/.data)…"), this,
|
||||||
|
[this] { emit radarImportRequested(false); });
|
||||||
|
radar->addAction(QStringLiteral("Impulse 测线目录(.iprb)…"), this,
|
||||||
|
[this] { emit radarImportRequested(true); });
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
bool TopBar::eventFilter(QObject* obj, QEvent* event) {
|
bool TopBar::eventFilter(QObject* obj, QEvent* event) {
|
||||||
if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) {
|
if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) {
|
||||||
if (userMenu_)
|
if (userMenu_)
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,13 @@ signals:
|
||||||
// 项目管理菜单中「直接嵌入」的 web 页被点击:title=窗口标题,target=嵌入页 target 路径。
|
// 项目管理菜单中「直接嵌入」的 web 页被点击:title=窗口标题,target=嵌入页 target 路径。
|
||||||
void webPageRequested(const QString& title, const QString& target);
|
void webPageRequested(const QString& title, const QString& target);
|
||||||
void analysisViewRequested(); // 视图菜单「分析视图」→ 中央区切回默认工作台
|
void analysisViewRequested(); // 视图菜单「分析视图」→ 中央区切回默认工作台
|
||||||
|
// 设备菜单「导入雷达测线」(后端未就绪的过渡测试入口):false=规范化(.head/.data),true=Impulse(.iprb)。
|
||||||
|
void radarImportRequested(bool impulse);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QMenu* buildViewMenu(); // 视图菜单(成员:「分析视图」需 emit 信号)
|
QMenu* buildViewMenu(); // 视图菜单(成员:「分析视图」需 emit 信号)
|
||||||
QMenu* buildProjectMenu(); // 项目管理菜单(成员:webview 叶子项需 emit 信号)
|
QMenu* buildProjectMenu(); // 项目管理菜单(成员:webview 叶子项需 emit 信号)
|
||||||
|
QMenu* buildDeviceMenu(); // 设备菜单(成员:「导入雷达测线」子项需 emit 信号)
|
||||||
|
|
||||||
QToolButton* wsBtn_ = nullptr;
|
QToolButton* wsBtn_ = nullptr;
|
||||||
QToolButton* projBtn_ = nullptr;
|
QToolButton* projBtn_ = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "VtkSceneView.hpp"
|
#include "VtkSceneView.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -9,17 +10,26 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include <vtkActor.h>
|
#include <vtkActor.h>
|
||||||
|
#include <vtkBillboardTextActor3D.h>
|
||||||
|
#include <vtkCallbackCommand.h>
|
||||||
|
#include <vtkCamera.h>
|
||||||
|
#include <vtkCommand.h>
|
||||||
#include <vtkProperty.h>
|
#include <vtkProperty.h>
|
||||||
#include <vtkBoundingBox.h>
|
#include <vtkBoundingBox.h>
|
||||||
#include <vtkCellPicker.h>
|
|
||||||
#include <vtkCubeAxesActor.h>
|
#include <vtkCubeAxesActor.h>
|
||||||
|
#include <vtkLineSource.h>
|
||||||
#include <vtkNew.h>
|
#include <vtkNew.h>
|
||||||
|
#include <vtkPolyDataMapper.h>
|
||||||
#include <vtkProp.h>
|
#include <vtkProp.h>
|
||||||
|
#include <vtkPropPicker.h>
|
||||||
|
#include <vtkTextProperty.h>
|
||||||
#include <vtkPiecewiseFunction.h>
|
#include <vtkPiecewiseFunction.h>
|
||||||
#include <vtkColorTransferFunction.h>
|
#include <vtkColorTransferFunction.h>
|
||||||
#include <vtkGPUVolumeRayCastMapper.h>
|
#include <vtkGPUVolumeRayCastMapper.h>
|
||||||
#include <vtkRenderWindow.h>
|
#include <vtkRenderWindow.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
|
#include <vtkSphereSource.h>
|
||||||
#include <vtkVolume.h>
|
#include <vtkVolume.h>
|
||||||
#include <vtkVolumeProperty.h>
|
#include <vtkVolumeProperty.h>
|
||||||
|
|
||||||
|
|
@ -77,6 +87,25 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render
|
||||||
// 近裁剪容差调小:场景含近处剖面 + 远处底图(几十km),默认容差会把近裁剪面随远面推出去、
|
// 近裁剪容差调小:场景含近处剖面 + 远处底图(几十km),默认容差会把近裁剪面随远面推出去、
|
||||||
// 切掉离相机近的剖面。调小后近面可贴近,剖面不被切(代价:远处深度精度略降,不可见层无所谓)。
|
// 切掉离相机近的剖面。调小后近面可贴近,剖面不被切(代价:远处深度精度略降,不可见层无所谓)。
|
||||||
scene_.renderer()->SetNearClippingPlaneTolerance(1e-5);
|
scene_.renderer()->SetNearClippingPlaneTolerance(1e-5);
|
||||||
|
ensureGnomon(); // 交互器若已就绪即装配角落方向标(否则首帧 render 时补装)
|
||||||
|
}
|
||||||
|
|
||||||
|
VtkSceneView::~VtkSceneView() {
|
||||||
|
// 摘除左键/相机观察者(clientData=this,本对象析构后若留存会悬垂)+ 移除叠加渲染器。
|
||||||
|
// 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。
|
||||||
|
if (renderWindow_) {
|
||||||
|
if (auto* iren = renderWindow_->GetInteractor()) {
|
||||||
|
if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_);
|
||||||
|
if (gnomonHoverTag_ != 0) iren->RemoveObserver(gnomonHoverTag_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gnomonClickTag_ = 0;
|
||||||
|
gnomonHoverTag_ = 0;
|
||||||
|
// 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。
|
||||||
|
if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_);
|
||||||
|
gnomonCamTag_ = 0;
|
||||||
|
gnomonObservedCam_ = nullptr;
|
||||||
|
if (renderWindow_ && gnomonRenderer_) renderWindow_->RemoveRenderer(gnomonRenderer_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
|
void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
|
||||||
|
|
@ -110,8 +139,7 @@ void VtkSceneView::clear() {
|
||||||
// 只移除数据 prop(按 ds 跟踪)+ 杂项(地形/测线)+ 坐标轴;不动底图(TileBasemap 自管)→ 重建不丢图。
|
// 只移除数据 prop(按 ds 跟踪)+ 杂项(地形/测线)+ 坐标轴;不动底图(TileBasemap 自管)→ 重建不丢图。
|
||||||
for (auto& kv : dsProps_) removeProps(kv.second);
|
for (auto& kv : dsProps_) removeProps(kv.second);
|
||||||
dsProps_.clear();
|
dsProps_.clear();
|
||||||
mapLineDs_.clear(); // 2D 足迹维度记录随数据图元一并清(模式标志保留)
|
mapLineDs_.clear(); // 2D 足迹归属记录随数据图元一并清
|
||||||
selectedMapLines_.clear(); // 选中态随图元清(actor 已销毁);Z 偏移 mapLineZOffset_ 保留→重建后复位高度
|
|
||||||
removeProps(miscProps_);
|
removeProps(miscProps_);
|
||||||
clearAnomalies(); // 异常 actor 随清场一并移除
|
clearAnomalies(); // 异常 actor 随清场一并移除
|
||||||
if (currentAxes_) {
|
if (currentAxes_) {
|
||||||
|
|
@ -123,6 +151,7 @@ void VtkSceneView::clear() {
|
||||||
volumeOwnerDs_.clear();
|
volumeOwnerDs_.clear();
|
||||||
volumes_.clear(); // 多体并发:清场移除所有体 image
|
volumes_.clear(); // 多体并发:清场移除所有体 image
|
||||||
frameAnchoredToData_ = false; // 新一轮选择重新按其首个真实剖面重锚原点
|
frameAnchoredToData_ = false; // 新一轮选择重新按其首个真实剖面重锚原点
|
||||||
|
useFittedAxes_ = false; // 清场:贴合轴复位为全场景轴(选中随数据一并失效,防残留旧盒)
|
||||||
if (onVolumeChanged) onVolumeChanged();
|
if (onVolumeChanged) onVolumeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +193,6 @@ void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid&
|
||||||
auto curtain = geopro::render::buildCurtain(grid, cs, *frame_);
|
auto curtain = geopro::render::buildCurtain(grid, cs, *frame_);
|
||||||
if (curtain) {
|
if (curtain) {
|
||||||
curtain->SetScale(1.0, 1.0, verticalExaggeration_); // 纵向夸张成墙
|
curtain->SetScale(1.0, 1.0, verticalExaggeration_); // 纵向夸张成墙
|
||||||
curtain->SetVisibility(analysisMode2D_ ? 0 : 1); // 帘面=3D内容:二维分析下隐藏
|
|
||||||
scene_.addActor(curtain);
|
scene_.addActor(curtain);
|
||||||
dsProps_[dsId].push_back(curtain);
|
dsProps_[dsId].push_back(curtain);
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +234,6 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume
|
||||||
// 体 actor 不参与拾取:切片选中靠点中切片平面(widget 交互/拾取)。否则点击落到体内部时
|
// 体 actor 不参与拾取:切片选中靠点中切片平面(widget 交互/拾取)。否则点击落到体内部时
|
||||||
// picker 命中体、worldPoint 落体内 → nearestSlice 按平面距离选错切片(用户 ④ 串选)。
|
// picker 命中体、worldPoint 落体内 → nearestSlice 按平面距离选错切片(用户 ④ 串选)。
|
||||||
volume->PickableOff();
|
volume->PickableOff();
|
||||||
volume->SetVisibility(analysisMode2D_ ? 0 : 1); // 体=3D内容:二维分析下隐藏
|
|
||||||
scene_.addViewProp(volume);
|
scene_.addViewProp(volume);
|
||||||
dsProps_[dsId].push_back(volume);
|
dsProps_[dsId].push_back(volume);
|
||||||
currentVolumeImage_ = image;
|
currentVolumeImage_ = image;
|
||||||
|
|
@ -228,7 +255,6 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume
|
||||||
auto iso = geopro::render::buildIsosurface(image, cs, vol.vmin, vol.vmax, isoVal);
|
auto iso = geopro::render::buildIsosurface(image, cs, vol.vmin, vol.vmax, isoVal);
|
||||||
if (iso) {
|
if (iso) {
|
||||||
iso->PickableOff(); // 不参与拾取(同体 actor,避免串选)
|
iso->PickableOff(); // 不参与拾取(同体 actor,避免串选)
|
||||||
iso->SetVisibility(analysisMode2D_ ? 0 : 1); // 3D 内容:二维分析下隐藏
|
|
||||||
scene_.addActor(iso);
|
scene_.addActor(iso);
|
||||||
dsProps_[dsId].push_back(iso);
|
dsProps_[dsId].push_back(iso);
|
||||||
}
|
}
|
||||||
|
|
@ -258,17 +284,29 @@ void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLi
|
||||||
// worldZ 已是最终世界高程(含摆放语义),不再施加 VE(足迹是水平线,非随深度的竖直图元)。
|
// worldZ 已是最终世界高程(含摆放语义),不再施加 VE(足迹是水平线,非随深度的竖直图元)。
|
||||||
// 足迹可能是首个(且唯一)带经纬的数据 → 与帘面同样重锚原点,否则按样本默认原点投到数百公里外不可见。
|
// 足迹可能是首个(且唯一)带经纬的数据 → 与帘面同样重锚原点,否则按样本默认原点投到数百公里外不可见。
|
||||||
anchorFrameIfNeeded(line.lat, line.lon, static_cast<int>(line.lat.size()));
|
anchorFrameIfNeeded(line.lat, line.lon, static_cast<int>(line.lat.size()));
|
||||||
auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_);
|
// 折线几何建于 Z=0,平面高程 worldZ 经 actor SetPosition 施加 → 后续拖 z 值滑块只改 position 即直接平移,
|
||||||
|
// 无需移除+异步重载几何(setMapLinesZ 走此)。首勾/后续勾选在当前平面 z 加入者立即摆到该 z。
|
||||||
|
auto actor = geopro::render::buildMapLine(line.lat, line.lon, 0.0, *frame_);
|
||||||
if (actor) {
|
if (actor) {
|
||||||
actor->SetVisibility(analysisMode2D_ ? 1 : 0); // 足迹=2D内容:仅二维分析下显示
|
actor->SetPosition(0.0, 0.0, worldZ);
|
||||||
auto off = mapLineZOffset_.find(dsId); // B 期:复用持久 Z 偏移(全量重建后仍在该高度)
|
|
||||||
if (off != mapLineZOffset_.end()) actor->AddPosition(0.0, 0.0, off->second);
|
|
||||||
scene_.addActor(actor);
|
scene_.addActor(actor);
|
||||||
dsProps_[dsId].push_back(actor);
|
dsProps_[dsId].push_back(actor);
|
||||||
mapLineDs_.insert(dsId); // 记录此 ds 为 2D 足迹(切 tab 按维度翻可见)
|
mapLineDs_.insert(dsId); // 记录此 ds 为 2D 足迹(供足迹归属识别)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::setMapLinesZ(const std::vector<std::string>& dsIds, double z) {
|
||||||
|
// 直接平移足迹:仅对属于足迹的 dsId 改其 actor 的 SetPosition(0,0,z),即时渲染,无移除+重载。
|
||||||
|
for (const auto& dsId : dsIds) {
|
||||||
|
if (!mapLineDs_.count(dsId)) continue;
|
||||||
|
auto it = dsProps_.find(dsId);
|
||||||
|
if (it == dsProps_.end()) continue;
|
||||||
|
for (auto& prop : it->second)
|
||||||
|
if (auto* a = vtkActor::SafeDownCast(prop)) a->SetPosition(0.0, 0.0, z);
|
||||||
|
}
|
||||||
|
if (renderWindow_) renderWindow_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
|
void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
|
||||||
auto terrain = geopro::render::buildTerrain(paths.demPath, paths.imagePath, *frame_, zRefElev_,
|
auto terrain = geopro::render::buildTerrain(paths.demPath, paths.imagePath, *frame_, zRefElev_,
|
||||||
verticalExaggeration_);
|
verticalExaggeration_);
|
||||||
|
|
@ -417,135 +455,315 @@ void VtkSceneView::fitView() {
|
||||||
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
|
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneView::setAnalysisMode2D(bool is2D) {
|
bool VtkSceneView::datasetBounds(const std::vector<std::string>& dsIds, double outB[6]) const {
|
||||||
if (is2D == analysisMode2D_) return; // 幂等:同模式重复切不做事
|
// computeDataBounds 的按 dsId 版:只并集给定 dsIds 的已渲染 actor 包围盒(同样仅计可见 prop)。
|
||||||
analysisMode2D_ = is2D;
|
vtkBoundingBox bb;
|
||||||
if (!is2D) clearMapLineSelection(); // 离开二维分析:清足迹选中(三维下不可拖 Z);Z 偏移仍持久
|
for (const auto& id : dsIds) {
|
||||||
|
auto it = dsProps_.find(id);
|
||||||
// ① 按维度翻可见标志(不清空、不重建→切换瞬时):2D 足迹↔3D 帘面/体;异常属 3D。
|
|
||||||
// 地形/测线(miscProps_)与底图(TileBasemap 自管)两边常驻、不动。
|
|
||||||
for (auto& kv : dsProps_) {
|
|
||||||
const bool is2dContent = mapLineDs_.count(kv.first) > 0;
|
|
||||||
const bool vis = is2D ? is2dContent : !is2dContent;
|
|
||||||
for (auto& p : kv.second)
|
|
||||||
if (p) p->SetVisibility(vis ? 1 : 0);
|
|
||||||
}
|
|
||||||
for (auto& kv : anomalyProps_)
|
|
||||||
if (kv.second) kv.second->SetVisibility(is2D ? 0 : 1); // 异常=3D内容
|
|
||||||
|
|
||||||
// ② 取景 + 坐标轴 + 渲染统一走 render():朝向按 analysisMode2D_(已设)选近俯视/自由透视;
|
|
||||||
// ResetCamera 到"可见"数据包围盒(computeDataBounds 只计可见 prop);rebuildAxes 在二维下自移除;
|
|
||||||
// 末尾 Render + onCameraChanged(底图按新视锥重算)。不再用相机快照(陈旧易错),每次按可见内容取景。
|
|
||||||
render(/*is2D ViewMode=*/false, /*resetCamera=*/true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 二维分析改造 B 期:选中 2D 足迹沿高程 Z 拖动 ───────────────────────────────────
|
|
||||||
void VtkSceneView::applyMapLineSelectionVisual() {
|
|
||||||
for (auto& kv : dsProps_) {
|
|
||||||
if (!mapLineDs_.count(kv.first)) continue;
|
|
||||||
const bool sel = selectedMapLines_.count(kv.first) > 0;
|
|
||||||
for (auto& p : kv.second) {
|
|
||||||
auto* a = vtkActor::SafeDownCast(p);
|
|
||||||
if (!a) continue;
|
|
||||||
if (sel) { // 选中:黄高亮 + 加粗
|
|
||||||
a->GetProperty()->SetColor(1.0, 0.85, 0.2);
|
|
||||||
a->GetProperty()->SetLineWidth(6.0);
|
|
||||||
} else { // 未选:复原 buildMapLine 默认(橙 3.0)
|
|
||||||
a->GetProperty()->SetColor(0.95, 0.55, 0.10);
|
|
||||||
a->GetProperty()->SetLineWidth(3.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VtkSceneView::clearMapLineSelection() {
|
|
||||||
if (selectedMapLines_.empty()) return;
|
|
||||||
selectedMapLines_.clear();
|
|
||||||
applyMapLineSelectionVisual();
|
|
||||||
if (renderWindow_) renderWindow_->Render();
|
|
||||||
if (onMapLineSelectionChanged) onMapLineSelectionChanged(); // VTK→列表:同步清空
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> VtkSceneView::selectedMapLines() const {
|
|
||||||
return std::vector<std::string>(selectedMapLines_.begin(), selectedMapLines_.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
void VtkSceneView::setSelectedMapLines(const std::vector<std::string>& dsIds) {
|
|
||||||
// 列表→VTK:按 dsId 设选中(仅已渲染足迹),高亮+渲染;不回调 onMapLineSelectionChanged(防回环)。
|
|
||||||
selectedMapLines_.clear();
|
|
||||||
for (const auto& id : dsIds)
|
|
||||||
if (mapLineDs_.count(id)) selectedMapLines_.insert(id);
|
|
||||||
applyMapLineSelectionVisual();
|
|
||||||
if (renderWindow_) renderWindow_->Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VtkSceneView::pickMapLineAt(int screenX, int screenY, bool additive) {
|
|
||||||
auto* ren = scene_.renderer();
|
|
||||||
if (!ren) return false;
|
|
||||||
// 只在"可见足迹"中拾取(PickFromList):避免地形/底图/隐藏的 3D 体抢命中。
|
|
||||||
vtkNew<vtkCellPicker> picker;
|
|
||||||
picker->SetTolerance(0.012);
|
|
||||||
picker->PickFromListOn();
|
|
||||||
bool any = false;
|
|
||||||
for (auto& kv : dsProps_) {
|
|
||||||
if (!mapLineDs_.count(kv.first)) continue;
|
|
||||||
for (auto& p : kv.second)
|
|
||||||
if (p && p->GetVisibility()) { picker->AddPickList(p); any = true; }
|
|
||||||
}
|
|
||||||
if (!any) return false; // 无可见足迹 → 不拦截(交由平移)
|
|
||||||
if (!picker->Pick(screenX, screenY, 0.0, ren)) {
|
|
||||||
if (!additive) clearMapLineSelection(); // 点空白(非多选)→ 取消选中
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
vtkProp* hit = picker->GetViewProp();
|
|
||||||
std::string hitDs;
|
|
||||||
for (auto& kv : dsProps_) {
|
|
||||||
if (!mapLineDs_.count(kv.first)) continue;
|
|
||||||
for (auto& p : kv.second)
|
|
||||||
if (p.Get() == hit) { hitDs = kv.first; break; }
|
|
||||||
if (!hitDs.empty()) break;
|
|
||||||
}
|
|
||||||
if (hitDs.empty()) {
|
|
||||||
if (!additive) clearMapLineSelection();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (additive) { // Ctrl 多选:切换该足迹
|
|
||||||
if (selectedMapLines_.count(hitDs)) selectedMapLines_.erase(hitDs);
|
|
||||||
else selectedMapLines_.insert(hitDs);
|
|
||||||
} else if (!selectedMapLines_.count(hitDs)) { // 单击未选中的线 → 替换为它
|
|
||||||
selectedMapLines_.clear();
|
|
||||||
selectedMapLines_.insert(hitDs);
|
|
||||||
}
|
|
||||||
// 单击已选中的线(可能为多选之一):保持当前选中集 → 起手即可整体拖动,不塌缩为单选。
|
|
||||||
applyMapLineSelectionVisual();
|
|
||||||
if (renderWindow_) renderWindow_->Render();
|
|
||||||
if (onMapLineSelectionChanged) onMapLineSelectionChanged(); // VTK→列表:同步选中
|
|
||||||
return !selectedMapLines_.empty(); // 有选中 → 交互样式进入 Z 拖动
|
|
||||||
}
|
|
||||||
|
|
||||||
void VtkSceneView::nudgeSelectedMapLinesZ(double worldDz) {
|
|
||||||
if (selectedMapLines_.empty() || worldDz == 0.0) return;
|
|
||||||
for (const auto& dsId : selectedMapLines_) {
|
|
||||||
mapLineZOffset_[dsId] += worldDz; // 持久累计(全量重建后 addMapLine 复用)
|
|
||||||
auto it = dsProps_.find(dsId);
|
|
||||||
if (it == dsProps_.end()) continue;
|
if (it == dsProps_.end()) continue;
|
||||||
for (auto& p : it->second) {
|
for (const auto& p : it->second)
|
||||||
auto* a = vtkActor::SafeDownCast(p);
|
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||||
if (a) a->AddPosition(0.0, 0.0, worldDz); // 仅改 Z,锁 XY
|
|
||||||
}
|
}
|
||||||
}
|
if (!bb.IsValid()) return false;
|
||||||
if (scene_.renderer()) scene_.renderer()->ResetCameraClippingRange(); // Z 抬升后防被裁剪面切
|
bb.GetBounds(outB);
|
||||||
if (renderWindow_) renderWindow_->Render();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
double VtkSceneView::selectedMapLineZ() const {
|
void VtkSceneView::fitToBounds(const double b[6]) {
|
||||||
if (selectedMapLines_.empty()) return 0.0;
|
if (!scene_.renderer()) return;
|
||||||
// 代表性 Z = 任一选中足迹 actor 的包围盒中心 Z(含 placement worldZ + 已累计偏移)。
|
scene_.renderer()->ResetCamera(b); // 保持朝向,仅重定位+缩放到该盒(区别于 fitView 的全场景)
|
||||||
auto it = dsProps_.find(*selectedMapLines_.begin());
|
scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉
|
||||||
if (it == dsProps_.end()) return 0.0;
|
if (renderWindow_) renderWindow_->Render();
|
||||||
for (const auto& p : it->second)
|
if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖
|
||||||
if (p) { if (double* b = p->GetBounds()) return 0.5 * (b[4] + b[5]); }
|
}
|
||||||
return 0.0;
|
|
||||||
|
void VtkSceneView::orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]) {
|
||||||
|
auto* r = scene_.renderer();
|
||||||
|
if (!r) return;
|
||||||
|
auto* c = r->GetActiveCamera();
|
||||||
|
// 保留当前缩放:取现距离 d=|cam−focal|,绕 pivot 转到 dir 轴(focal=pivot、pos=pivot+off*d)。
|
||||||
|
const double d = c->GetDistance();
|
||||||
|
const auto pose = geopro::render::orbitPose(toRenderViewDir(dir), pivot, d);
|
||||||
|
c->SetFocalPoint(pose.focal[0], pose.focal[1], pose.focal[2]);
|
||||||
|
c->SetPosition(pose.pos[0], pose.pos[1], pose.pos[2]);
|
||||||
|
c->SetViewUp(pose.up[0], pose.up[1], pose.up[2]);
|
||||||
|
c->OrthogonalizeViewUp();
|
||||||
|
r->ResetCameraClippingRange(); // 只转向不改距离 → 不 ResetCamera;仅扩裁剪面
|
||||||
|
if (renderWindow_) renderWindow_->Render();
|
||||||
|
if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::orbitToCurrentPivot(geopro::controller::ViewDir dir) {
|
||||||
|
// 支点 = 当前坐标轴盒中心(决策 5):有选中(贴合轴)→选中子树盒 fittedBounds_;否则全场景数据盒。
|
||||||
|
double b[6];
|
||||||
|
if (useFittedAxes_) {
|
||||||
|
for (int i = 0; i < 6; ++i) b[i] = fittedBounds_[i];
|
||||||
|
} else if (!computeDataBounds(b)) {
|
||||||
|
return; // 无有效数据包围盒 → 无支点可绕,静默不动
|
||||||
|
}
|
||||||
|
const double pivot[3] = {0.5 * (b[0] + b[1]), 0.5 * (b[2] + b[3]), 0.5 * (b[4] + b[5])};
|
||||||
|
orbitToAxis(dir, pivot); // 复用 T1:绕 pivot 转到 dir 轴、保留当前缩放
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::ensureGnomon() {
|
||||||
|
// 幂等装配:交互器就绪后建一次。用【专用叠加渲染器】(非 vtkOrientationMarkerWidget):图层1、固定
|
||||||
|
// 右下角视口、InteractiveOff、透明背景 → 无 widget 外框、不可拖动/缩放;相机由 syncGnomonCamera
|
||||||
|
// 镜像主相机朝向 → gizmo 随场景旋转同步转。三轴线 + 6 方向球(仅球可拾取) + 正向 XYZ 标签。
|
||||||
|
if (gnomonReady_ || !renderWindow_) return;
|
||||||
|
auto* iren = renderWindow_->GetInteractor();
|
||||||
|
if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装
|
||||||
|
|
||||||
|
// 叠加渲染器:图层1 固定右下角(见下 SetViewport) —— 避开底部满宽沿线滑块条(仅雷达体时显示、
|
||||||
|
// 约占底 46px)。透明背景只显 gizmo 图元;FXAA + MSAA 抗锯齿使边缘平滑;非交互不响应任何输入。
|
||||||
|
renderWindow_->SetNumberOfLayers(2);
|
||||||
|
gnomonRenderer_ = vtkSmartPointer<vtkRenderer>::New();
|
||||||
|
gnomonRenderer_->SetLayer(1);
|
||||||
|
gnomonRenderer_->InteractiveOff();
|
||||||
|
// 右下角、上抬避开底部满宽「沿线位置」滑块条(约占底 46px):y 从 0.10 抬到 0.15;靠右留 ~1% 边距。
|
||||||
|
gnomonRenderer_->SetViewport(0.855, 0.15, 0.995, 0.35);
|
||||||
|
gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块
|
||||||
|
gnomonRenderer_->SetUseFXAA(true); // FXAA + 窗口 MSAA 双重:轴线/球/字形边缘平滑
|
||||||
|
renderWindow_->AddRenderer(gnomonRenderer_);
|
||||||
|
|
||||||
|
// 抗锯齿(spec §7):整窗多重采样一次性开(仅当尚未开,不覆盖既有设置) → 平涂盘/白字边缘平滑。
|
||||||
|
if (renderWindow_->GetMultiSamples() == 0) renderWindow_->SetMultiSamples(8);
|
||||||
|
|
||||||
|
const double L = 1.0; // 球心到原点距离(= 轴线长度)
|
||||||
|
// 业界柔和轴色(非纯 RGB):X 红 / Y 绿 / Z 蓝(spec §2)。正向球=本色,负向球=本色×0.42(更暗)。
|
||||||
|
const std::array<double, 3> kColX = {0.90, 0.30, 0.36};
|
||||||
|
const std::array<double, 3> kColY = {0.55, 0.78, 0.33};
|
||||||
|
const std::array<double, 3> kColZ = {0.28, 0.45, 0.90};
|
||||||
|
|
||||||
|
// 三根过原点的轴线:仅连到【正向】球,X=红 / Y=绿 / Z=蓝(平涂纯色、细、不可拾取;spec §5)。
|
||||||
|
struct AxisLine { double to[3]; std::array<double, 3> col; };
|
||||||
|
const AxisLine lines[3] = {
|
||||||
|
{{L, 0, 0}, kColX}, // X 红
|
||||||
|
{{0, L, 0}, kColY}, // Y 绿
|
||||||
|
{{0, 0, L}, kColZ}, // Z 蓝
|
||||||
|
};
|
||||||
|
for (const auto& ln : lines) {
|
||||||
|
vtkNew<vtkLineSource> src;
|
||||||
|
src->SetPoint1(0.0, 0.0, 0.0);
|
||||||
|
src->SetPoint2(ln.to[0], ln.to[1], ln.to[2]);
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputConnection(src->GetOutputPort());
|
||||||
|
vtkNew<vtkActor> a;
|
||||||
|
a->SetMapper(mapper);
|
||||||
|
a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]);
|
||||||
|
a->GetProperty()->SetLineWidth(1.8f); // 细轴线
|
||||||
|
a->GetProperty()->LightingOff(); // 平涂纯色,无高光
|
||||||
|
a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义)
|
||||||
|
gnomonRenderer_->AddViewProp(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6 个方向球(平涂实心盘,非高光 3D 球):正向亮盘 + 白色 XYZ 字标、稍大;负向同色更暗、更小、无字标。
|
||||||
|
// 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、−Z=Bottom、+Y=Back、−Y=Front、+X=Right、−X=Left。
|
||||||
|
struct DirSpec {
|
||||||
|
geopro::controller::ViewDir dir;
|
||||||
|
double pos[3];
|
||||||
|
std::array<double, 3> base; // 该轴柔和本色
|
||||||
|
bool positive;
|
||||||
|
const char* label; // 正向字标;负向 nullptr
|
||||||
|
};
|
||||||
|
const DirSpec specs[6] = {
|
||||||
|
{geopro::controller::ViewDir::Right, {L, 0, 0}, kColX, true, "X"}, // +X
|
||||||
|
{geopro::controller::ViewDir::Left, {-L, 0, 0}, kColX, false, nullptr}, // −X
|
||||||
|
{geopro::controller::ViewDir::Back, {0, L, 0}, kColY, true, "Y"}, // +Y
|
||||||
|
{geopro::controller::ViewDir::Front, {0, -L, 0}, kColY, false, nullptr}, // −Y
|
||||||
|
{geopro::controller::ViewDir::Top, {0, 0, L}, kColZ, true, "Z"}, // +Z
|
||||||
|
{geopro::controller::ViewDir::Bottom, {0, 0, -L}, kColZ, false, nullptr}, // −Z
|
||||||
|
};
|
||||||
|
gnomonDirs_.clear();
|
||||||
|
gnomonBaseColor_.clear();
|
||||||
|
gnomonLabels_.clear();
|
||||||
|
for (const auto& s : specs) {
|
||||||
|
// 正向盘 r=0.32(稍大);负向盘 r=0.20(更小、更暗 → 呈"淡环/凹陷"观感,仍可拾取)。
|
||||||
|
const double radius = s.positive ? 0.32 : 0.20;
|
||||||
|
const std::array<double, 3> col =
|
||||||
|
s.positive ? s.base
|
||||||
|
: std::array<double, 3>{s.base[0] * 0.42, s.base[1] * 0.42, s.base[2] * 0.42};
|
||||||
|
vtkNew<vtkSphereSource> sphere;
|
||||||
|
sphere->SetRadius(radius);
|
||||||
|
sphere->SetThetaResolution(48); // 高分辨率 → 轮廓平滑(平涂下尤重要)
|
||||||
|
sphere->SetPhiResolution(48);
|
||||||
|
sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]);
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputConnection(sphere->GetOutputPort());
|
||||||
|
auto actor = vtkSmartPointer<vtkActor>::New();
|
||||||
|
actor->SetMapper(mapper);
|
||||||
|
auto* prop = actor->GetProperty();
|
||||||
|
prop->SetColor(col[0], col[1], col[2]);
|
||||||
|
prop->LightingOff(); // 关键(spec §1):平涂实心盘,无镜面/渐变 → 干净的 Blender 式 gizmo
|
||||||
|
actor->SetOrigin(s.pos[0], s.pos[1], s.pos[2]); // 缩放原点=球心 → hover 放大就地不位移
|
||||||
|
actor->SetPickable(1); // 6 球均可拾取(负向点击仍 orbit 到对侧)
|
||||||
|
gnomonRenderer_->AddViewProp(actor);
|
||||||
|
gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向
|
||||||
|
gnomonBaseColor_[actor.Get()] = col; // 记本色 → hover 提亮后可复原
|
||||||
|
|
||||||
|
if (s.positive && s.label) { // 正向轴字标:始终朝相机的公告板文字,白色粗体、居中于球心
|
||||||
|
auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::New();
|
||||||
|
lbl->SetInput(s.label);
|
||||||
|
lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]);
|
||||||
|
auto* tp = lbl->GetTextProperty();
|
||||||
|
tp->SetFontSize(20);
|
||||||
|
tp->SetBold(true);
|
||||||
|
tp->SetColor(1.0, 1.0, 1.0); // 白字
|
||||||
|
tp->SetJustificationToCentered();
|
||||||
|
tp->SetVerticalJustificationToCentered();
|
||||||
|
lbl->SetPickable(0);
|
||||||
|
gnomonRenderer_->AddViewProp(lbl);
|
||||||
|
gnomonLabels_.push_back({lbl.Get(),
|
||||||
|
{s.pos[0], s.pos[1], s.pos[2]}}); // syncGnomonCamera 推到球前避遮挡
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gnomonPicker_ = vtkSmartPointer<vtkPropPicker>::New();
|
||||||
|
|
||||||
|
// 左键高优先级(1.0)观察者:先于交互样式(0.0),命中方向球 → orbit + abort 消费(阻止相机旋转/拾取)。
|
||||||
|
gnomonClickCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
|
||||||
|
gnomonClickCmd_->SetClientData(this);
|
||||||
|
gnomonClickCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
|
||||||
|
static_cast<VtkSceneView*>(client)->handleGnomonClick();
|
||||||
|
});
|
||||||
|
gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0);
|
||||||
|
|
||||||
|
// 鼠标移动高优先级观察者:仅角落内拾取做 hover 高亮,永不 abort → 不阻塞场景旋转/平移/切片交互。
|
||||||
|
gnomonHoverCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
|
||||||
|
gnomonHoverCmd_->SetClientData(this);
|
||||||
|
gnomonHoverCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
|
||||||
|
static_cast<VtkSceneView*>(client)->handleGnomonHover();
|
||||||
|
});
|
||||||
|
gnomonHoverTag_ = iren->AddObserver(vtkCommand::MouseMoveEvent, gnomonHoverCmd_, 1.0);
|
||||||
|
|
||||||
|
// 相机同步:观察主相机 ModifiedEvent,每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。
|
||||||
|
gnomonCamCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
|
||||||
|
gnomonCamCmd_->SetClientData(this);
|
||||||
|
gnomonCamCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
|
||||||
|
static_cast<VtkSceneView*>(client)->syncGnomonCamera();
|
||||||
|
});
|
||||||
|
if (auto* mainCam = scene_.renderer() ? scene_.renderer()->GetActiveCamera() : nullptr) {
|
||||||
|
gnomonObservedCam_ = mainCam;
|
||||||
|
gnomonCamTag_ = mainCam->AddObserver(vtkCommand::ModifiedEvent, gnomonCamCmd_);
|
||||||
|
}
|
||||||
|
syncGnomonCamera(); // 初始一次:装配即与当前朝向对齐
|
||||||
|
|
||||||
|
gnomonReady_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::syncGnomonCamera() {
|
||||||
|
if (!gnomonRenderer_ || !scene_.renderer()) return;
|
||||||
|
auto* mainCam = scene_.renderer()->GetActiveCamera();
|
||||||
|
auto* gcam = gnomonRenderer_->GetActiveCamera();
|
||||||
|
if (!mainCam || !gcam) return;
|
||||||
|
// 复制主相机投影方向 + view-up:gizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。
|
||||||
|
double dir[3];
|
||||||
|
mainCam->GetDirectionOfProjection(dir); // 已归一化(F−P),指向场景内(背离相机)
|
||||||
|
double up[3];
|
||||||
|
mainCam->GetViewUp(up);
|
||||||
|
const double dist = 10.0;
|
||||||
|
gcam->SetParallelProjection(1); // 正交投影:gizmo 无透视畸变(业界标准)
|
||||||
|
// 按视口像素长宽比自适应取景半高:balls 到 ±(L+r)≈1.32 + 白字留边 → halfExtent=1.5。
|
||||||
|
// parallelScale = 视口世界半高;水平可见半宽 = scale×aspect。取 scale=halfExtent/min(1,aspect)
|
||||||
|
// 保证长/宽两向都容得下所有球 → 任意窗口长宽比不裁切(视口归一化随窗拉伸也不失效)。
|
||||||
|
const double halfExtent = 1.5;
|
||||||
|
double aspect = 1.0;
|
||||||
|
const int* wsz = renderWindow_->GetSize();
|
||||||
|
const double* vp = gnomonRenderer_->GetViewport();
|
||||||
|
if (wsz && wsz[0] > 0 && wsz[1] > 0) {
|
||||||
|
const double vpH = (vp[3] - vp[1]) * wsz[1];
|
||||||
|
if (vpH > 0.0) aspect = (vp[2] - vp[0]) * wsz[0] / vpH;
|
||||||
|
}
|
||||||
|
gcam->SetParallelScale(halfExtent / std::min(1.0, aspect));
|
||||||
|
gcam->SetFocalPoint(0.0, 0.0, 0.0);
|
||||||
|
gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist);
|
||||||
|
gcam->SetViewUp(up[0], up[1], up[2]);
|
||||||
|
gnomonRenderer_->ResetCameraClippingRange();
|
||||||
|
|
||||||
|
// 正向白字标签推到球【前】(朝相机 = −dir 方向、偏移略大于球半径):billboard 恒面相机,位于球前
|
||||||
|
// → 不被球面前半遮挡,读数清晰。相机每次转动都随之更新前向偏移。
|
||||||
|
const double front = 0.40; // > 正向球半径 0.32 → 字浮于球前表面之外
|
||||||
|
for (const auto& lp : gnomonLabels_) {
|
||||||
|
if (!lp.first) continue;
|
||||||
|
lp.first->SetPosition(lp.second[0] - dir[0] * front, lp.second[1] - dir[1] * front,
|
||||||
|
lp.second[2] - dir[2] * front);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::handleGnomonClick() {
|
||||||
|
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
|
||||||
|
auto* iren = renderWindow_->GetInteractor();
|
||||||
|
if (!iren) return;
|
||||||
|
const int ex = iren->GetEventPosition()[0];
|
||||||
|
const int ey = iren->GetEventPosition()[1];
|
||||||
|
// 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。
|
||||||
|
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
|
||||||
|
const int* sz = renderWindow_->GetSize();
|
||||||
|
if (sz[0] <= 0 || sz[1] <= 0) return;
|
||||||
|
const double fx = static_cast<double>(ex) / sz[0];
|
||||||
|
const double fy = static_cast<double>(ey) / sz[1];
|
||||||
|
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 不 abort,放行
|
||||||
|
// 角落内硬件拾取(仅方向球可拾):命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。
|
||||||
|
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
|
||||||
|
vtkProp* leaf = gnomonPicker_->GetViewProp(); // 叠加渲染器内为裸 actor,直取即方向球
|
||||||
|
auto it = gnomonDirs_.find(leaf);
|
||||||
|
if (it != gnomonDirs_.end()) {
|
||||||
|
orbitToCurrentPivot(it->second);
|
||||||
|
if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 命中才消费:不触发相机旋转/场景拾取
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 未命中球 → 不 abort:左键继续走正常交互(旋转/平移/缩放/切片/拾取),保证非干扰。
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::handleGnomonHover() {
|
||||||
|
// 非阻塞 hover 高亮:绝不 SetAbortFlag → 鼠标移动照常传给交互样式(旋转/平移/切片)。
|
||||||
|
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
|
||||||
|
auto* iren = renderWindow_->GetInteractor();
|
||||||
|
if (!iren) return;
|
||||||
|
|
||||||
|
// 提亮本色(向白混 0.42);复原用记录的本色。二者共用,避免重复。
|
||||||
|
auto applyColor = [](vtkProp* p, const std::array<double, 3>& c, bool highlight, double scale) {
|
||||||
|
auto* a = vtkActor::SafeDownCast(p);
|
||||||
|
if (!a) return;
|
||||||
|
if (highlight) {
|
||||||
|
a->GetProperty()->SetColor(c[0] * 0.58 + 0.42, c[1] * 0.58 + 0.42, c[2] * 0.58 + 0.42);
|
||||||
|
a->SetScale(scale); // 就地放大(origin=球心)
|
||||||
|
} else {
|
||||||
|
a->GetProperty()->SetColor(c[0], c[1], c[2]);
|
||||||
|
a->SetScale(1.0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto restore = [&]() {
|
||||||
|
if (!gnomonHovered_) return;
|
||||||
|
auto it = gnomonBaseColor_.find(gnomonHovered_);
|
||||||
|
if (it != gnomonBaseColor_.end()) applyColor(gnomonHovered_, it->second, false, 1.0);
|
||||||
|
gnomonHovered_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const int ex = iren->GetEventPosition()[0];
|
||||||
|
const int ey = iren->GetEventPosition()[1];
|
||||||
|
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
|
||||||
|
const int* sz = renderWindow_->GetSize();
|
||||||
|
if (sz[0] <= 0 || sz[1] <= 0) { restore(); return; }
|
||||||
|
const double fx = static_cast<double>(ex) / sz[0];
|
||||||
|
const double fy = static_cast<double>(ey) / sz[1];
|
||||||
|
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) { // 光标离开角落 → 复原并跳过拾取(廉价)
|
||||||
|
if (gnomonHovered_) { restore(); renderWindow_->Render(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 角落内才做硬件拾取:命中方向球 → 高亮该球、复原旧的;未命中 → 复原。
|
||||||
|
vtkProp* hit = nullptr;
|
||||||
|
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
|
||||||
|
vtkProp* leaf = gnomonPicker_->GetViewProp();
|
||||||
|
if (gnomonBaseColor_.find(leaf) != gnomonBaseColor_.end()) hit = leaf;
|
||||||
|
}
|
||||||
|
if (hit == gnomonHovered_) return; // 无变化 → 不重绘
|
||||||
|
restore();
|
||||||
|
if (hit) {
|
||||||
|
auto it = gnomonBaseColor_.find(hit);
|
||||||
|
if (it != gnomonBaseColor_.end()) applyColor(hit, it->second, true, 1.18);
|
||||||
|
gnomonHovered_ = hit;
|
||||||
|
}
|
||||||
|
renderWindow_->Render(); // 高亮/复原变更 → 立即刷新
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneView::rebuildAxes() {
|
void VtkSceneView::rebuildAxes() {
|
||||||
|
|
@ -555,13 +773,15 @@ void VtkSceneView::rebuildAxes() {
|
||||||
scene_.renderer()->RemoveViewProp(currentAxes_);
|
scene_.renderer()->RemoveViewProp(currentAxes_);
|
||||||
currentAxes_ = nullptr;
|
currentAxes_ = nullptr;
|
||||||
}
|
}
|
||||||
// 二维分析无立体坐标轴:任何渲染路径(全量/增量/切模式)走到此都不重建坐标轴,
|
|
||||||
// 保证切到二维分析后坐标轴消失、且后续增量渲染不把它带回来。
|
|
||||||
if (analysisMode2D_) return;
|
|
||||||
// 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大),
|
// 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大),
|
||||||
// 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr,场景无坐标轴。
|
// 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr,场景无坐标轴。
|
||||||
|
// 贴合态(useFittedAxes_):改用选中子树盒 fittedBounds_,只框该子树而非全场景(spec §3.2)。
|
||||||
double bounds[6];
|
double bounds[6];
|
||||||
if (!computeDataBounds(bounds)) return; // 无数据 → 不建坐标轴
|
if (useFittedAxes_) {
|
||||||
|
for (int i = 0; i < 6; ++i) bounds[i] = fittedBounds_[i];
|
||||||
|
} else if (!computeDataBounds(bounds)) {
|
||||||
|
return; // 无数据 → 不建坐标轴
|
||||||
|
}
|
||||||
geopro::render::AxesOptions opts;
|
geopro::render::AxesOptions opts;
|
||||||
opts.mode = toRenderMode(axesMode_);
|
opts.mode = toRenderMode(axesMode_);
|
||||||
opts.unit = toRenderUnit(axesUnit_);
|
opts.unit = toRenderUnit(axesUnit_);
|
||||||
|
|
@ -580,7 +800,23 @@ void VtkSceneView::rebuildAxes() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::showFittedAxes(const double b[6]) {
|
||||||
|
// 选中子树盒 → 冻结为贴合轴 bounds,隐去全场景轴(rebuildAxes 会先移除旧轴再按 fittedBounds_ 重建)。
|
||||||
|
useFittedAxes_ = true;
|
||||||
|
for (int i = 0; i < 6; ++i) fittedBounds_[i] = b[i];
|
||||||
|
rebuildAxes();
|
||||||
|
if (renderWindow_) renderWindow_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::showSceneAxes() {
|
||||||
|
// 取消选中 → 复位为全场景总览轴(现状默认)。清掉贴合态后 rebuildAxes 走 computeDataBounds。
|
||||||
|
useFittedAxes_ = false;
|
||||||
|
rebuildAxes();
|
||||||
|
if (renderWindow_) renderWindow_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
void VtkSceneView::render(bool is2D, bool resetCamera) {
|
void VtkSceneView::render(bool is2D, bool resetCamera) {
|
||||||
|
ensureGnomon(); // 构造时交互器未就绪则于此补装(幂等)
|
||||||
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
|
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
|
||||||
double bgR, bgG, bgB;
|
double bgR, bgG, bgB;
|
||||||
geopro::app::vtkBackground(bgR, bgG, bgB);
|
geopro::app::vtkBackground(bgR, bgG, bgB);
|
||||||
|
|
@ -588,12 +824,9 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
|
||||||
// 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。
|
// 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。
|
||||||
if (!is2D) rebuildAxes();
|
if (!is2D) rebuildAxes();
|
||||||
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
|
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
|
||||||
// 朝向优先看二维分析模式(本期 A):处二维分析→近俯视;否则按 ViewMode(旧 Map2D 正俯视/三维自由)。
|
// 朝向按 is2D:俯视(Map2D)/三维自由透视。
|
||||||
// 这样 VE 改/项目切等全量重建在二维分析下仍保持近俯视,不跳回三维自由视角。
|
|
||||||
if (resetCamera) {
|
if (resetCamera) {
|
||||||
if (analysisMode2D_)
|
if (is2D)
|
||||||
geopro::render::applyNearTop2D(scene_.renderer());
|
|
||||||
else if (is2D)
|
|
||||||
geopro::render::applyTop2D(scene_.renderer());
|
geopro::render::applyTop2D(scene_.renderer());
|
||||||
else
|
else
|
||||||
geopro::render::applyFree3D(scene_.renderer());
|
geopro::render::applyFree3D(scene_.renderer());
|
||||||
|
|
@ -609,6 +842,7 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneView::renderIncremental() {
|
void VtkSceneView::renderIncremental() {
|
||||||
|
ensureGnomon(); // 幂等:交互器就绪后补装角落方向标
|
||||||
// 增量渲染:仅按新包围盒重建坐标轴并提交,不动相机(勾选/取消时视角不跳)。
|
// 增量渲染:仅按新包围盒重建坐标轴并提交,不动相机(勾选/取消时视角不跳)。
|
||||||
rebuildAxes();
|
rebuildAxes();
|
||||||
scene_.renderer()->ResetCameraClippingRange(); // 数据/底图变化后扩裁剪面,防被切
|
scene_.renderer()->ResetCameraClippingRange(); // 数据/底图变化后扩裁剪面,防被切
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <vtkCubeAxesActor.h>
|
#include <vtkCubeAxesActor.h>
|
||||||
|
|
@ -20,6 +22,10 @@ class vtkRenderWindow;
|
||||||
class vtkProp;
|
class vtkProp;
|
||||||
class vtkActor;
|
class vtkActor;
|
||||||
class vtkVolume;
|
class vtkVolume;
|
||||||
|
class vtkPropPicker;
|
||||||
|
class vtkCallbackCommand;
|
||||||
|
class vtkCamera;
|
||||||
|
class vtkBillboardTextActor3D;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -32,6 +38,7 @@ public:
|
||||||
// 入参生命周期须覆盖本对象(由调用方保证)。zRefElev:地形 z 基准(测线地表高程)。
|
// 入参生命周期须覆盖本对象(由调用方保证)。zRefElev:地形 z 基准(测线地表高程)。
|
||||||
VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow,
|
VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow,
|
||||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double zRefElev);
|
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double zRefElev);
|
||||||
|
~VtkSceneView() override; // 摘除 gnomon 左键/相机观察者(clientData=this),移除叠加渲染器
|
||||||
|
|
||||||
void clear() override;
|
void clear() override;
|
||||||
void setVerticalExaggeration(double ve) override;
|
void setVerticalExaggeration(double ve) override;
|
||||||
|
|
@ -46,6 +53,7 @@ public:
|
||||||
const geopro::core::ColorScale& cs) override;
|
const geopro::core::ColorScale& cs) override;
|
||||||
void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
||||||
double worldZ) override;
|
double worldZ) override;
|
||||||
|
void setMapLinesZ(const std::vector<std::string>& dsIds, double z) override;
|
||||||
void addTerrain(const geopro::data::TerrainPaths& paths) override;
|
void addTerrain(const geopro::data::TerrainPaths& paths) override;
|
||||||
void removeDataset(const std::string& dsId) override;
|
void removeDataset(const std::string& dsId) override;
|
||||||
void addAnomaly(const geopro::core::Anomaly& a) override;
|
void addAnomaly(const geopro::core::Anomaly& a) override;
|
||||||
|
|
@ -61,6 +69,24 @@ public:
|
||||||
void applyCameraView(geopro::controller::ViewDir dir) override;
|
void applyCameraView(geopro::controller::ViewDir dir) override;
|
||||||
void zoom(double factor) override;
|
void zoom(double factor) override;
|
||||||
void fitView() override;
|
void fitView() override;
|
||||||
|
|
||||||
|
// ── 视图导航基元(spec §3.1;T1)──────────────────────────────────────────────
|
||||||
|
// 给定 dsIds 的已渲染 actor 世界包围盒并集;无有效返回 false,否则填 out=[xmin,xmax,…,zmax]。
|
||||||
|
bool datasetBounds(const std::vector<std::string>& dsIds, double outB[6]) const;
|
||||||
|
// 相机适配到指定包围盒,保持当前朝向(ResetCamera(b)),用于双击适配/贴合。
|
||||||
|
void fitToBounds(const double b[6]);
|
||||||
|
// 绕 pivot 转到沿 dir 轴看向 pivot,保留当前 focal-to-camera 距离(缩放不变)。
|
||||||
|
void orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]);
|
||||||
|
// ── 贴合轴 / 全景轴(spec §3.2;T2)───────────────────────────────────────────
|
||||||
|
// 选中某 ds → 用其子树盒 b 显示贴合 cube axes、隐去全场景总览轴(立即提交渲染)。
|
||||||
|
void showFittedAxes(const double b[6]);
|
||||||
|
// 取消选中 → 恢复全场景总览轴(现状默认行为,立即提交渲染)。
|
||||||
|
void showSceneAxes();
|
||||||
|
// ── 可点击方向标 gnomon(spec §3.3;T3)─────────────────────────────────────────
|
||||||
|
// 绕【当前坐标轴盒中心】转到 dir 轴、保留当前缩放:支点 = 有选中(useFittedAxes_)→选中子树盒
|
||||||
|
// fittedBounds_ 中心,否则全场景数据盒 computeDataBounds 中心。无有效数据 → no-op。
|
||||||
|
// 封装决策 5 的支点规则,调用方只需给方向(角落 gnomon 点击即调此)。
|
||||||
|
void orbitToCurrentPivot(geopro::controller::ViewDir dir);
|
||||||
void render(bool is2D, bool resetCamera = true) override;
|
void render(bool is2D, bool resetCamera = true) override;
|
||||||
void renderIncremental() override;
|
void renderIncremental() override;
|
||||||
|
|
||||||
|
|
@ -87,13 +113,6 @@ public:
|
||||||
// 相机程序化变化(取景/预设/缩放)后回调,供底图按新视锥重算覆盖(否则首帧部分瓦片要手动微动才出)。
|
// 相机程序化变化(取景/预设/缩放)后回调,供底图按新视锥重算覆盖(否则首帧部分瓦片要手动微动才出)。
|
||||||
std::function<void()> onCameraChanged;
|
std::function<void()> onCameraChanged;
|
||||||
|
|
||||||
// ── 二维分析改造 A 期:一场景两相机 ──────────────────────────────────────────
|
|
||||||
// 切「二维分析」(is2D=true):相机锁近俯视、显 2D 足迹/隐 3D(体/帘面/异常);切回反之。
|
|
||||||
// 只翻 actor 可见标志(不清空、不重建)→ 切换瞬时、零重插值。地形/底图常驻不动。
|
|
||||||
// 切片显隐 + 交互锁由 InteractionManager::setMode2D 配合(上层在同一处调两者)。
|
|
||||||
void setAnalysisMode2D(bool is2D);
|
|
||||||
bool isAnalysisMode2D() const { return analysisMode2D_; }
|
|
||||||
|
|
||||||
// ── B 方案#2:沿线位置巡航(雷达超长测线)──────────────────────────────────────
|
// ── B 方案#2:沿线位置巡航(雷达超长测线)──────────────────────────────────────
|
||||||
// t∈[0,1] 沿数据【最长轴】定位;取景到该位置一段【窗口】(windowFrac=窗口占长轴比例),
|
// t∈[0,1] 沿数据【最长轴】定位;取景到该位置一段【窗口】(windowFrac=窗口占长轴比例),
|
||||||
// 保持当前朝向(ResetCamera 只重定位+缩放、不转向)→ 像滚动读长 radargram。短轴满幅、长轴只取一段。
|
// 保持当前朝向(ResetCamera 只重定位+缩放、不转向)→ 像滚动读长 radargram。短轴满幅、长轴只取一段。
|
||||||
|
|
@ -101,22 +120,6 @@ public:
|
||||||
// 数据包围盒长短轴比(max/min 跨度)。用于判是否细长(雷达)→ 决定沿线滑块显隐。无数据返回 0。
|
// 数据包围盒长短轴比(max/min 跨度)。用于判是否细长(雷达)→ 决定沿线滑块显隐。无数据返回 0。
|
||||||
double longAxisElongation() const;
|
double longAxisElongation() const;
|
||||||
|
|
||||||
// ── 二维分析改造 B 期:选中 2D 足迹沿高程 Z 拖动 ───────────────────────────────
|
|
||||||
// 仅二维分析下用。pickMapLineAt:在屏幕(x,y)拾取足迹(只考虑可见足迹,不被地形/底图干扰);命中则
|
|
||||||
// 选中(additive=Ctrl 多选切换,否则单选替换)并高亮,返回是否有选中(交互样式据此决定 Z 拖动/平移)。
|
|
||||||
// nudgeSelectedMapLinesZ:选中足迹世界 Z += worldDz(锁 XY);偏移按 dsId 持久(切走再回/全量重建保留)。
|
|
||||||
// selectedMapLineZ:代表性当前世界 Z(高程读数浮层用);无选中返回 0。
|
|
||||||
bool pickMapLineAt(int screenX, int screenY, bool additive);
|
|
||||||
void clearMapLineSelection();
|
|
||||||
bool hasMapLineSelection() const { return !selectedMapLines_.empty(); }
|
|
||||||
void nudgeSelectedMapLinesZ(double worldDz);
|
|
||||||
double selectedMapLineZ() const;
|
|
||||||
// 双向选择联动:列表↔VTK。selectedMapLines 取当前选中 dsId;setSelectedMapLines 由列表设置选中
|
|
||||||
// (高亮,不回调,避免环)。VTK 内拾取改变选中时触发 onMapLineSelectionChanged → 上层同步列表。
|
|
||||||
std::vector<std::string> selectedMapLines() const;
|
|
||||||
void setSelectedMapLines(const std::vector<std::string>& dsIds);
|
|
||||||
std::function<void()> onMapLineSelectionChanged;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁
|
// 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁
|
||||||
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
|
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
|
||||||
|
|
@ -126,6 +129,20 @@ private:
|
||||||
void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
|
void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
|
||||||
// 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。
|
// 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。
|
||||||
bool computeDataBounds(double out[6]) const;
|
bool computeDataBounds(double out[6]) const;
|
||||||
|
// 角落可点击方向标 gnomon(T3):首次(交互器就绪)时装配【专用叠加渲染器】(图层1、固定右下角、
|
||||||
|
// 非交互无边框) + 三轴线 + 6 向可拾取球 + 正向标签 + 左键/相机观察者。
|
||||||
|
// 幂等:装配后置 gnomonReady_,重复调直接返回。render/renderIncremental/构造均可安全调用。
|
||||||
|
void ensureGnomon();
|
||||||
|
// 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort
|
||||||
|
// (消费事件,阻止相机旋转/场景拾取);否则不 abort,放行正常交互(旋转/平移/缩放/切片/拾取)。
|
||||||
|
void handleGnomonClick();
|
||||||
|
// 鼠标移动(非 abort、不阻塞场景交互)回调:仅当光标落在 gnomon 角落视口内才拾取,命中方向球 →
|
||||||
|
// 高亮(提亮本色 + 放大 ~1.18×),复原其余;离开角落或未命中 → 复原全部。picking 只在角落内进行(廉价)。
|
||||||
|
void handleGnomonHover();
|
||||||
|
// 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点),
|
||||||
|
// 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。
|
||||||
|
// 同时按视口像素长宽比自适应取景半高(球始终不裁切) + 把正向标签推到球前(朝相机)避免被球面遮挡。
|
||||||
|
void syncGnomonCamera();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。
|
// 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。
|
||||||
|
|
@ -150,6 +167,10 @@ private:
|
||||||
// 当前坐标轴 prop:render 可能多次调用 rebuildAxes(rebuild 末尾 + 异步回灌),
|
// 当前坐标轴 prop:render 可能多次调用 rebuildAxes(rebuild 末尾 + 异步回灌),
|
||||||
// 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。
|
// 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。
|
||||||
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
|
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
|
||||||
|
// 贴合轴态(T2):true=坐标轴按 fittedBounds_(选中子树盒)建,非全场景数据包围盒;选中时冻结该盒,
|
||||||
|
// 取消/清场复位为 false(走全场景 computeDataBounds)。
|
||||||
|
bool useFittedAxes_ = false;
|
||||||
|
double fittedBounds_[6] = {0, 0, 0, 0, 0, 0};
|
||||||
|
|
||||||
// 当前体素 image + 色阶(P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/
|
// 当前体素 image + 色阶(P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/
|
||||||
// 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。
|
// 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。
|
||||||
|
|
@ -183,15 +204,32 @@ private:
|
||||||
std::string volumeOwnerDs_; // 当前 currentVolumeImage_ 归属的 ds(其被移除时置空切片源)
|
std::string volumeOwnerDs_; // 当前 currentVolumeImage_ 归属的 ds(其被移除时置空切片源)
|
||||||
std::map<std::string, vtkSmartPointer<vtkActor>> anomalyProps_; // 异常 id → 3D actor
|
std::map<std::string, vtkSmartPointer<vtkActor>> anomalyProps_; // 异常 id → 3D actor
|
||||||
|
|
||||||
// ── 二维分析改造 A 期 ──
|
// 哪些 dsProps_ 条目是 2D 足迹(addMapLine):供足迹 actor 归属识别(Task E2/F2 用)。
|
||||||
// 哪些 dsProps_ 条目是 2D 足迹(addMapLine):切 tab 按此区分维度翻可见(其余 dsProps_=帘面/体=3D)。
|
|
||||||
std::set<std::string> mapLineDs_;
|
std::set<std::string> mapLineDs_;
|
||||||
bool analysisMode2D_ = false; // 当前是否处二维分析(默认三维:启动在「三维分析」tab)
|
|
||||||
|
|
||||||
// B 期:选中的足迹 dsId(Z 拖动目标) + 各足迹累计 Z 偏移(持久,全量重建后 addMapLine 复用)。
|
// ── 可点击方向标 gnomon(T3)──────────────────────────────────────────────────
|
||||||
std::set<std::string> selectedMapLines_;
|
// 专用叠加渲染器:图层1、固定右下角视口、InteractiveOff、透明背景、无边框 —— 不是 widget,
|
||||||
std::map<std::string, double> mapLineZOffset_;
|
// 故无外框、不可拖动/缩放;相机由 syncGnomonCamera 镜像主相机朝向 → gizmo 随场景转。
|
||||||
void applyMapLineSelectionVisual(); // 选中足迹加粗变亮、其余复原(橙 3.0)
|
// gnomonPicker_ 在此渲染器上做硬件拾取(仅方向球可拾取)。
|
||||||
|
vtkSmartPointer<vtkRenderer> gnomonRenderer_;
|
||||||
|
vtkSmartPointer<vtkPropPicker> gnomonPicker_;
|
||||||
|
vtkSmartPointer<vtkCallbackCommand> gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费)
|
||||||
|
unsigned long gnomonClickTag_ = 0; // 左键观察者句柄(析构时摘除)
|
||||||
|
vtkSmartPointer<vtkCallbackCommand> gnomonCamCmd_; // 主相机 ModifiedEvent 观察者命令
|
||||||
|
unsigned long gnomonCamTag_ = 0; // 相机观察者句柄(析构时摘除)
|
||||||
|
vtkCamera* gnomonObservedCam_ = nullptr; // 被观察的主相机(非拥有;析构摘观察者用)
|
||||||
|
bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon)
|
||||||
|
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向)。actor 由叠加渲染器持有保活。
|
||||||
|
std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_;
|
||||||
|
|
||||||
|
// ── hover 高亮(spec §6)─────────────────────────────────────────────────────
|
||||||
|
vtkSmartPointer<vtkCallbackCommand> gnomonHoverCmd_; // 鼠标移动观察者命令(不 abort,非阻塞)
|
||||||
|
unsigned long gnomonHoverTag_ = 0; // 移动观察者句柄(析构摘除)
|
||||||
|
vtkProp* gnomonHovered_ = nullptr; // 当前高亮的方向球(裸指针,renderer 保活)
|
||||||
|
std::map<vtkProp*, std::array<double, 3>> gnomonBaseColor_; // 各球本色(hover 复原用)
|
||||||
|
// 正向标签(白字) + 其球心:每次 syncGnomonCamera 把标签推到球前(朝相机)→ 不被球面遮挡。
|
||||||
|
// raw ptr 非拥有,由叠加渲染器持有保活(与 gnomonDirs_ 同生命周期约定)。
|
||||||
|
std::vector<std::pair<vtkBillboardTextActor3D*, std::array<double, 3>>> gnomonLabels_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMenu>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidgetAction>
|
||||||
|
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
|
@ -52,9 +54,28 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
|
||||||
col->addWidget(line);
|
col->addWidget(line);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 段1:设置(坐标轴)──
|
// ── 段1:设置(坐标轴)/ 底图 ──
|
||||||
connect(iconBtn(Glyph::Gear, QStringLiteral("坐标轴设置")), &QToolButton::clicked, this,
|
connect(iconBtn(Glyph::Gear, QStringLiteral("坐标轴设置")), &QToolButton::clicked, this,
|
||||||
&VtkViewToolbar::axesSettingsRequested);
|
&VtkViewToolbar::axesSettingsRequested);
|
||||||
|
// 共享 3D 底图控件:天地图/无 + 透明度滑块(spec §7.5/§9.1,从数据集栏移至渲染区工具条,全局唯一)。
|
||||||
|
{
|
||||||
|
auto* mapBtn = iconBtn(Glyph::Map, QStringLiteral("底图"));
|
||||||
|
mapBtn->setPopupMode(QToolButton::InstantPopup);
|
||||||
|
auto* menu = new QMenu(mapBtn);
|
||||||
|
menu->addAction(QStringLiteral("天地图"), this, [this] { emit basemapKindChanged(0); });
|
||||||
|
menu->addAction(QStringLiteral("无"), this, [this] { emit basemapKindChanged(1); });
|
||||||
|
menu->addSeparator();
|
||||||
|
auto* wa = new QWidgetAction(menu);
|
||||||
|
auto* sld = new QSlider(Qt::Horizontal, menu);
|
||||||
|
sld->setRange(0, 100);
|
||||||
|
sld->setValue(50);
|
||||||
|
sld->setToolTip(QStringLiteral("底图透明度"));
|
||||||
|
connect(sld, &QSlider::valueChanged, this,
|
||||||
|
[this](int v) { emit basemapOpacityChanged(v / 100.0); });
|
||||||
|
wa->setDefaultWidget(sld);
|
||||||
|
menu->addAction(wa);
|
||||||
|
mapBtn->setMenu(menu);
|
||||||
|
}
|
||||||
sep();
|
sep();
|
||||||
// ── 段2:快捷视图(前/后/上/下/左/右)──
|
// ── 段2:快捷视图(前/后/上/下/左/右)──
|
||||||
struct V {
|
struct V {
|
||||||
|
|
@ -67,7 +88,6 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
|
||||||
const ViewDir d = v.d;
|
const ViewDir d = v.d;
|
||||||
auto* b = textBtn(QString::fromUtf8(v.t));
|
auto* b = textBtn(QString::fromUtf8(v.t));
|
||||||
connect(b, &QToolButton::clicked, this, [this, d] { emit viewRequested(d); });
|
connect(b, &QToolButton::clicked, this, [this, d] { emit viewRequested(d); });
|
||||||
viewDirButtons_.push_back(b); // 二维分析下禁用(会改朝向、破坏近俯视锁定)
|
|
||||||
}
|
}
|
||||||
sep();
|
sep();
|
||||||
// ── 段3:缩放 / 复位 ──(三维体不透明度已移交色阶「不透明度」,旧「透」滑块退役移除)
|
// ── 段3:缩放 / 复位 ──(三维体不透明度已移交色阶「不透明度」,旧「透」滑块退役移除)
|
||||||
|
|
@ -89,12 +109,4 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
|
||||||
adjustSize();
|
adjustSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkViewToolbar::setAnalysisMode2D(bool is2D) {
|
|
||||||
for (auto* b : viewDirButtons_) {
|
|
||||||
if (!b) continue;
|
|
||||||
b->setEnabled(!is2D);
|
|
||||||
b->setToolTip(is2D ? QStringLiteral("二维分析下不可用(已锁定近俯视)") : QString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <vector>
|
|
||||||
#include "I3dSceneView.hpp" // geopro::controller::ViewDir
|
#include "I3dSceneView.hpp" // geopro::controller::ViewDir
|
||||||
|
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
|
|
@ -9,27 +8,21 @@ class QLabel;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// VTK 画布竖排工具条(spec §9):全局视图控制——设置(坐标轴)/前后上下左右/放大缩小复位。
|
// VTK 画布竖排工具条(spec §9):全局视图控制——设置(坐标轴)/底图/前后上下左右/放大缩小复位。
|
||||||
// 仅发信号,不认 VTK;由 main 接到场景控制器。
|
// 仅发信号,不认 VTK;由 main 接到场景控制器与共享 3D 底图。
|
||||||
class VtkViewToolbar : public QWidget {
|
class VtkViewToolbar : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit VtkViewToolbar(QWidget* parent = nullptr);
|
explicit VtkViewToolbar(QWidget* parent = nullptr);
|
||||||
|
|
||||||
public slots:
|
|
||||||
// 二维分析激活时禁用不适用的工具:6 向快捷视图会改相机朝向→破坏二维近俯视锁定,故二维下禁用;
|
|
||||||
// 缩放/适配/坐标轴设置(含 VE)仍可用。切回三维恢复。
|
|
||||||
void setAnalysisMode2D(bool is2D);
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void axesSettingsRequested(); // 设置 → 弹 AxesSettingsDialog
|
void axesSettingsRequested(); // 设置 → 弹 AxesSettingsDialog
|
||||||
|
void basemapKindChanged(int kind); // 底图类型:0 天地图 / 1 无
|
||||||
|
void basemapOpacityChanged(double o); // 底图透明度:0..1
|
||||||
void viewRequested(geopro::controller::ViewDir dir); // 前/后/上/下/左/右
|
void viewRequested(geopro::controller::ViewDir dir); // 前/后/上/下/左/右
|
||||||
void zoomInRequested();
|
void zoomInRequested();
|
||||||
void zoomOutRequested();
|
void zoomOutRequested();
|
||||||
void fitRequested(); // 复位=适配
|
void fitRequested(); // 复位=适配
|
||||||
|
|
||||||
private:
|
|
||||||
std::vector<QToolButton*> viewDirButtons_; // 6 向快捷视图按钮:二维分析下禁用
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
366
src/app/main.cpp
366
src/app/main.cpp
|
|
@ -93,8 +93,8 @@
|
||||||
|
|
||||||
#include "ApiClient.hpp"
|
#include "ApiClient.hpp"
|
||||||
#include "AuthService.hpp"
|
#include "AuthService.hpp"
|
||||||
#include "DatasetDimension.hpp"
|
|
||||||
#include "DatasetCategory.hpp"
|
#include "DatasetCategory.hpp"
|
||||||
|
#include "repo/CategoryDescriptor.hpp"
|
||||||
#include "Credential.hpp"
|
#include "Credential.hpp"
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
#include "Logging.hpp"
|
#include "Logging.hpp"
|
||||||
|
|
@ -142,6 +142,7 @@
|
||||||
#include "panels/DatasetAttrPanel.hpp"
|
#include "panels/DatasetAttrPanel.hpp"
|
||||||
#include "panels/ObjectExceptionPanel.hpp"
|
#include "panels/ObjectExceptionPanel.hpp"
|
||||||
#include "TileBasemap.hpp"
|
#include "TileBasemap.hpp"
|
||||||
|
#include "TileBasemapPlaneAdapter.hpp"
|
||||||
#include "panels/columns/ColumnDrawer.hpp"
|
#include "panels/columns/ColumnDrawer.hpp"
|
||||||
#include "panels/columns/CategoryAnalysisTab.hpp"
|
#include "panels/columns/CategoryAnalysisTab.hpp"
|
||||||
#include "panels/columns/CategorySection.hpp"
|
#include "panels/columns/CategorySection.hpp"
|
||||||
|
|
@ -149,7 +150,6 @@
|
||||||
#include "AxesSettingsDialog.hpp"
|
#include "AxesSettingsDialog.hpp"
|
||||||
#include "AxesSettingsPanel.hpp"
|
#include "AxesSettingsPanel.hpp"
|
||||||
#include "repo/DatasetFieldDictionary.hpp"
|
#include "repo/DatasetFieldDictionary.hpp"
|
||||||
#include "panels/columns/Column2DDataset.hpp"
|
|
||||||
|
|
||||||
#include "CameraPreset.hpp"
|
#include "CameraPreset.hpp"
|
||||||
#include "ColorLutBuilder.hpp"
|
#include "ColorLutBuilder.hpp"
|
||||||
|
|
@ -482,27 +482,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 子视图(表头+左树+画布)并 raise 到顶层;保存阶段显示,完成即撤。复用于其他需要整窗等待的场景。
|
// 子视图(表头+左树+画布)并 raise 到顶层;保存阶段显示,完成即撤。复用于其他需要整窗等待的场景。
|
||||||
auto* vtkLoading = new geopro::app::LoadingOverlay(centerWidget);
|
auto* vtkLoading = new geopro::app::LoadingOverlay(centerWidget);
|
||||||
|
|
||||||
// ── 二维分析 B 期:高程 Z 拖动读数浮层(顶部居中)+ 足迹拾取/拖动回调注入交互样式 ──────────
|
|
||||||
// 拖动选中足迹时显示其当前世界 Z,松开隐藏;不挡画布鼠标。深底方角(同异常提示坑规避)。
|
|
||||||
auto* elevHint = new QLabel(vtkWidget);
|
|
||||||
elevHint->setObjectName(QStringLiteral("elevHint"));
|
|
||||||
elevHint->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
geopro::app::applyTokenizedStyleSheet(
|
|
||||||
elevHint, QStringLiteral("QLabel#elevHint{background:#0E1A2D;color:#E6ECF5;"
|
|
||||||
"border:1px solid {{accent/primary}};padding:6px 12px;}"));
|
|
||||||
elevHint->hide();
|
|
||||||
// 滚轮升降时读数浮层 1.2s 后自动隐藏(拖动则在松开时隐藏)。
|
|
||||||
auto* zHideTimer = new QTimer(vtkWidget);
|
|
||||||
zHideTimer->setSingleShot(true);
|
|
||||||
QObject::connect(zHideTimer, &QTimer::timeout, elevHint, [elevHint]() { elevHint->hide(); });
|
|
||||||
auto showZReadout = std::make_shared<std::function<void()>>([sceneView, elevHint, vtkWidget]() {
|
|
||||||
elevHint->setText(
|
|
||||||
QStringLiteral("高程 Z:%1 m").arg(sceneView->selectedMapLineZ(), 0, 'f', 1));
|
|
||||||
elevHint->adjustSize();
|
|
||||||
elevHint->move((vtkWidget->width() - elevHint->width()) / 2, 12); // 顶部居中
|
|
||||||
elevHint->show();
|
|
||||||
elevHint->raise();
|
|
||||||
});
|
|
||||||
// ── B 方案#2:雷达沿线位置滑块(超长测线巡航)────────────────────────────────────
|
// ── B 方案#2:雷达沿线位置滑块(超长测线巡航)────────────────────────────────────
|
||||||
// 拖动 → 相机沿数据最长轴 dolly 到该位置的一段窗口(focusAlongLongAxis)。仅细长(雷达)体显示。
|
// 拖动 → 相机沿数据最长轴 dolly 到该位置的一段窗口(focusAlongLongAxis)。仅细长(雷达)体显示。
|
||||||
auto* alongLineBar = new QWidget(vtkWidget);
|
auto* alongLineBar = new QWidget(vtkWidget);
|
||||||
|
|
@ -528,48 +507,22 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto* alongLineOverlay = new BottomBarOverlay(alongLineBar, vtkWidget);
|
auto* alongLineOverlay = new BottomBarOverlay(alongLineBar, vtkWidget);
|
||||||
QObject::connect(alongLineSlider, &QSlider::valueChanged, vtkWidget,
|
QObject::connect(alongLineSlider, &QSlider::valueChanged, vtkWidget,
|
||||||
[sceneView](int v) { sceneView->focusAlongLongAxis(v / 1000.0, 0.12); });
|
[sceneView](int v) { sceneView->focusAlongLongAxis(v / 1000.0, 0.12); });
|
||||||
// 显隐刷新:仅三维分析 + 细长(长短轴比≥4,即雷达)体时显示沿线滑块。
|
// 显隐刷新:仅当场景中实际渲染了雷达三维体(StoredVolume 带 linePrefix)时显示沿线滑块。
|
||||||
|
// 旧逻辑靠数据包围盒长短轴比≥4 判定,统一自由场景下细长的非雷达数据(2D 轨迹线/反演帘面/
|
||||||
|
// 普通体)会撑大合并包围盒→误触发。改按「已渲染体中是否存在雷达体」门控(雷达体取消勾选/移除
|
||||||
|
// 后 onVolumeChanged 会重评并隐藏)。
|
||||||
auto refreshAlongLineBar = std::make_shared<std::function<void()>>(
|
auto refreshAlongLineBar = std::make_shared<std::function<void()>>(
|
||||||
[sceneView, alongLineBar, alongLineOverlay]() {
|
[sceneView, scene3dRepo, alongLineBar, alongLineOverlay]() {
|
||||||
const bool show = !sceneView->isAnalysisMode2D() && sceneView->longAxisElongation() >= 4.0;
|
bool show = false;
|
||||||
|
for (const auto& kv : sceneView->volumes())
|
||||||
|
if (scene3dRepo->isRadarVolume(kv.first)) {
|
||||||
|
show = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
alongLineBar->setVisible(show);
|
alongLineBar->setVisible(show);
|
||||||
if (show) alongLineOverlay->reposition();
|
if (show) alongLineOverlay->reposition();
|
||||||
});
|
});
|
||||||
|
// 双向选择联动(B2:去 col2D 后由统一段 datasetSelected 承担 2D 选中,此处旧 col2D 链已移除)。
|
||||||
if (auto* style = interactionMgr->pickStyle()) {
|
|
||||||
// 命中可见足迹→选中(Ctrl 多选)并返回是否进入 Z 拖动;未命中(返回 false)→交互样式回退平移。
|
|
||||||
style->onPick2D = [sceneView](int x, int y, bool additive) {
|
|
||||||
return sceneView->pickMapLineAt(x, y, additive);
|
|
||||||
};
|
|
||||||
// 拖动中:施加世界 Z 增量(仅改 Z),并把选中足迹当前高程显示在顶部读数浮层。
|
|
||||||
style->onDrag2D = [sceneView, showZReadout](double worldDz) {
|
|
||||||
sceneView->nudgeSelectedMapLinesZ(worldDz);
|
|
||||||
(*showZReadout)();
|
|
||||||
};
|
|
||||||
style->onDrag2DEnd = [elevHint]() { elevHint->hide(); };
|
|
||||||
// 滚轮升降:有选中足迹则施加 Z 增量并显示读数(1.2s 后自动隐藏),返回 true 消费滚轮;否则缩放。
|
|
||||||
style->onWheel2D = [sceneView, showZReadout, zHideTimer](double worldDz) {
|
|
||||||
if (!sceneView->hasMapLineSelection()) return false;
|
|
||||||
sceneView->nudgeSelectedMapLinesZ(worldDz);
|
|
||||||
(*showZReadout)();
|
|
||||||
zHideTimer->start(1200);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 双向选择联动:列表行选中 ↔ VTK 足迹高亮。两向各自屏蔽回环(setSelectedMapLines 不回调、
|
|
||||||
// setSelectedDsIds 屏蔽信号),故无需额外守卫。
|
|
||||||
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::selectedDatasetsChanged, &window,
|
|
||||||
[sceneView](const QStringList& ids) {
|
|
||||||
std::vector<std::string> v;
|
|
||||||
for (const QString& s : ids) v.push_back(s.toStdString());
|
|
||||||
sceneView->setSelectedMapLines(v);
|
|
||||||
});
|
|
||||||
sceneView->onMapLineSelectionChanged = [sceneView, drawer]() {
|
|
||||||
QStringList ids;
|
|
||||||
for (const std::string& s : sceneView->selectedMapLines())
|
|
||||||
ids << QString::fromStdString(s);
|
|
||||||
drawer->col2D()->setSelectedDsIds(ids);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 坐标轴设置抽屉面板:叠加 vtkWidget、工具条右侧滑出,默认隐藏(点设置 toggle)。
|
// 坐标轴设置抽屉面板:叠加 vtkWidget、工具条右侧滑出,默认隐藏(点设置 toggle)。
|
||||||
auto* axesPanel = new geopro::app::AxesSettingsPanel(vtkWidget);
|
auto* axesPanel = new geopro::app::AxesSettingsPanel(vtkWidget);
|
||||||
|
|
@ -650,7 +603,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// (设计 §2.1:三维分析栏按 对象/三维体模型/切片 三级树),归三维分析栏(非数据集栏)。
|
// (设计 §2.1:三维分析栏按 对象/三维体模型/切片 三级树),归三维分析栏(非数据集栏)。
|
||||||
// 后端列表每次勾选测线全量覆盖,故客户端体单独维护、刷新时合并注入(不被冲掉)。
|
// 后端列表每次勾选测线全量覆盖,故客户端体单独维护、刷新时合并注入(不被冲掉)。
|
||||||
// 三维分析数据源 = 最近对象树勾选拉取的 ds + 客户端三维体(mock) + 已保存切片;
|
// 三维分析数据源 = 最近对象树勾选拉取的 ds + 客户端三维体(mock) + 已保存切片;
|
||||||
// splitByCategory 后注入 5 段(电阻率/视电阻率/瞬变/三维体/切片);二维(足迹)经 dim2D 仍走 col2D。
|
// splitByCategory 后注入各段(电阻率/视电阻率/瞬变/三维体/轨迹);二维(足迹/轨迹)并入同一单列(B2)。
|
||||||
auto lastSourceRows = std::make_shared<std::vector<geopro::data::DsRow>>();
|
auto lastSourceRows = std::make_shared<std::vector<geopro::data::DsRow>>();
|
||||||
auto lastStructNodes = std::make_shared<std::vector<geopro::data::StructNode>>(); // 生成位置候选(项目内 GS/TM)
|
auto lastStructNodes = std::make_shared<std::vector<geopro::data::StructNode>>(); // 生成位置候选(项目内 GS/TM)
|
||||||
auto refreshAnalysis = [drawer, scene3dRepo, lastSourceRows, lastStructNodes]() {
|
auto refreshAnalysis = [drawer, scene3dRepo, lastSourceRows, lastStructNodes]() {
|
||||||
|
|
@ -667,7 +620,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
for (const auto& s : slices) voxelTree.push_back(s);
|
for (const auto& s : slices) voxelTree.push_back(s);
|
||||||
for (const auto& a : anomalies) voxelTree.push_back(a);
|
for (const auto& a : anomalies) voxelTree.push_back(a);
|
||||||
if (auto* sec = drawer->analysisTab()->section("voxel")) sec->setDatasets(voxelTree);
|
if (auto* sec = drawer->analysisTab()->section("voxel")) sec->setDatasets(voxelTree);
|
||||||
drawer->col2D()->setDatasets(geopro::app::splitByDimension(*lastSourceRows).dim2D);
|
// B2:二维(足迹/轨迹)不再走 col2D;splitByCategory 已含 trajectory 段,随 setBuckets 自动入单列。
|
||||||
|
drawer->analysisTab()->refreshVisibility(); // voxel 段单独喂数据后刷新分段可见性(Task B1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染勾选聚合:三维数据集栏(剖面→帘面)+ 三维分析栏(三维体/切片→体素/切片)两套勾选并集
|
// 渲染勾选聚合:三维数据集栏(剖面→帘面)+ 三维分析栏(三维体/切片→体素/切片)两套勾选并集
|
||||||
|
|
@ -678,10 +632,31 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 让「三维分析栏勾选(体/切片)」这条渲染路径也能隐藏不透明引导层——否则它盖住已渲染的体
|
// 让「三维分析栏勾选(体/切片)」这条渲染路径也能隐藏不透明引导层——否则它盖住已渲染的体
|
||||||
// (雷达体由分析栏勾选触发渲染,但旧逻辑只在对象树勾选时隐藏引导层 → 体被盖住看不到)。
|
// (雷达体由分析栏勾选触发渲染,但旧逻辑只在对象树勾选时隐藏引导层 → 体被盖住看不到)。
|
||||||
auto setSceneEmptyVisible = std::make_shared<std::function<void(bool)>>();
|
auto setSceneEmptyVisible = std::make_shared<std::function<void(bool)>>();
|
||||||
auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis, setSceneEmptyVisible]() {
|
// 勾选并集 → (dsId, typeId=描述符 id) 列表 → 控制器统一入口(B2:经描述符路由渲染策略,无维度散判)。
|
||||||
|
// typeId 解析:三维体(mock,不在 lastSourceRows) → "voxel";其余(剖面/轨迹)在 lastSourceRows
|
||||||
|
// 按 categoryCatalog().classify 命中描述符 → 其 id。控制器据 catalog[typeId].renderStrategyId 派策略。
|
||||||
|
auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis, setSceneEmptyVisible,
|
||||||
|
scene3dRepo, lastSourceRows]() {
|
||||||
QStringList all = *checkedProfiles;
|
QStringList all = *checkedProfiles;
|
||||||
all += *checkedAnalysis;
|
all += *checkedAnalysis;
|
||||||
sceneCtrl->setCheckedDatasets(all);
|
std::vector<std::pair<std::string, std::string>> idType;
|
||||||
|
const auto& cat = geopro::data::categoryCatalog();
|
||||||
|
for (const QString& q : all) {
|
||||||
|
const std::string id = q.toStdString();
|
||||||
|
std::string typeId;
|
||||||
|
if (scene3dRepo->isVolumeDataset(id)) {
|
||||||
|
typeId = "voxel"; // 客户端三维体(mock) 不在 lastSourceRows,按仓储谓词直判
|
||||||
|
} else {
|
||||||
|
const auto& rows = *lastSourceRows; // 剖面/轨迹按描述符 classify 解析具体类型
|
||||||
|
auto it = std::find_if(rows.begin(), rows.end(),
|
||||||
|
[&](const geopro::data::DsRow& r) { return r.id == id; });
|
||||||
|
if (it != rows.end())
|
||||||
|
for (const auto& d : cat)
|
||||||
|
if (d.classify && d.classify(*it)) { typeId = d.id; break; }
|
||||||
|
}
|
||||||
|
if (!typeId.empty()) idType.push_back({id, typeId});
|
||||||
|
}
|
||||||
|
sceneCtrl->setCheckedDatasets(idType);
|
||||||
if (*setSceneEmptyVisible) (*setSceneEmptyVisible)(all.isEmpty()); // 场景有内容→隐藏引导层
|
if (*setSceneEmptyVisible) (*setSceneEmptyVisible)(all.isEmpty()); // 场景有内容→隐藏引导层
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1052,66 +1027,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
analysisTab->scrollItemToTop(qid); // 新三维体行尽量滚到分析栏顶部
|
analysisTab->scrollItemToTop(qid); // 新三维体行尽量滚到分析栏顶部
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// 本地导入三维雷达测线(后端未就绪的过渡入口):入口=三维体段头「+ 导入雷达测线」按钮(CategorySection)
|
// 本地导入三维雷达测线(后端未就绪的过渡测试入口):入口已迁至 TopBar「设备」菜单
|
||||||
// → analysisTab.radarImportRequested(impulse)。app 无原生菜单栏(menuBar 被 TopBar 经 setMenuWidget 占用),
|
// →「导入雷达测线」二级项(emit TopBar::radarImportRequested)。接线在 topBar 创建后(见下方设备菜单接线),
|
||||||
// 故入口放可见的段头按钮。impulse=false 走规范化(.head/.data, 懒加载后台建体);true 走 Impulse(.iprb, eager)。
|
// 因 topBar 此处尚未构造;导入流程目标不变。
|
||||||
QObject::connect(
|
|
||||||
analysisTab, &geopro::app::CategoryAnalysisTab::radarImportRequested, &window,
|
|
||||||
[&window, scene3dRepo, refreshAnalysis, analysisTab, vtkLoading](bool impulse) {
|
|
||||||
if (!impulse) { // 规范化 .head/.data → registerRadarDataset(dd_radar_3d, 懒加载后台建体)
|
|
||||||
const QString dir = QFileDialog::getExistingDirectory(
|
|
||||||
&window, QStringLiteral("选择规范化三维雷达测线目录(含 *.head/*.data)"));
|
|
||||||
if (dir.isEmpty()) return;
|
|
||||||
bool ok = false;
|
|
||||||
const QString prefix = QInputDialog::getText(
|
|
||||||
&window, QStringLiteral("测线前缀"),
|
|
||||||
QStringLiteral("输入测线前缀(如 南同大道_000):"), QLineEdit::Normal, QString(), &ok);
|
|
||||||
if (!ok || prefix.isEmpty()) return;
|
|
||||||
// structParentId 暂空(P0 挂三维体段根;P1 接 TM 归属)。
|
|
||||||
// coarse=1 全分辨率(沿线不抽稀):验收期要肉眼判读反射/双曲线/通道连续性,
|
|
||||||
// 不能被沿线抽稀糊掉。单线峰值内存 ~0.7–1.5GB(spec §8.4);若 OOM 退回 2。
|
|
||||||
const std::string newId = scene3dRepo->registerRadarDataset(
|
|
||||||
dir.toLocal8Bit().toStdString(), prefix.toLocal8Bit().toStdString(),
|
|
||||||
prefix.toStdString(), /*structParentId=*/std::string(), /*coarse=*/1);
|
|
||||||
{ const QSignalBlocker block(analysisTab); refreshAnalysis(); } // DS 进三维体段(不触发渲染)
|
|
||||||
const QString qid = QString::fromStdString(newId);
|
|
||||||
analysisTab->setItemChecked(qid, true); // 勾选 → addDatasetAsync → loadVolume 后台建体渲染
|
|
||||||
analysisTab->setItemBusy(qid, true); // spinner; 渲染完成由 datasetRendered 撤
|
|
||||||
analysisTab->scrollItemToTop(qid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 明星路 Impulse(.iprb):复用现成 createGprVolume(eager 同步建体,预填 cachedGrid)。双数据集互证下游几何无关。
|
|
||||||
const QString dir = QFileDialog::getExistingDirectory(
|
|
||||||
&window, QStringLiteral("选择 Impulse 测线目录(含 *.iprb/*.ord)"));
|
|
||||||
if (dir.isEmpty()) return;
|
|
||||||
bool ok = false;
|
|
||||||
const QString prefix = QInputDialog::getText(
|
|
||||||
&window, QStringLiteral("测线前缀"),
|
|
||||||
QStringLiteral("输入测线前缀(如 明星路_010):"), QLineEdit::Normal, QString(), &ok);
|
|
||||||
if (!ok || prefix.isEmpty()) return;
|
|
||||||
vtkLoading->showOver(QStringLiteral("正在建Impulse体…"));
|
|
||||||
// 内层捕获 window 引用(非 [=] 值拷贝):QMainWindow 拷贝构造已删除,且 showToast 需非 const QWidget*。
|
|
||||||
QTimer::singleShot(0, &window, [=, &window]() {
|
|
||||||
std::string newId;
|
|
||||||
try {
|
|
||||||
newId = scene3dRepo->createGprVolume(dir.toLocal8Bit().toStdString(),
|
|
||||||
prefix.toLocal8Bit().toStdString(),
|
|
||||||
prefix.toStdString(), /*coarse=*/8);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
vtkLoading->hide();
|
|
||||||
geopro::app::showToast(&window,
|
|
||||||
QStringLiteral("建体失败:%1").arg(QString::fromLocal8Bit(e.what())));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
{ const QSignalBlocker block(analysisTab); refreshAnalysis(); }
|
|
||||||
vtkLoading->hide();
|
|
||||||
const QString qid = QString::fromStdString(newId);
|
|
||||||
// createGprVolume 预填 cachedGrid → setItemChecked 内 loadVolume 同步渲染、datasetRendered 自动撤 busy;
|
|
||||||
// 故此处【不要】再 setItemBusy(true)(否则 spinner 永久转圈)。
|
|
||||||
analysisTab->setItemChecked(qid, true);
|
|
||||||
analysisTab->scrollItemToTop(qid);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// 任一数据集(剖面/体)异步加载开始 → 列表项复选框转等待 spinner;渲染完成 → 复原复选框。
|
// 任一数据集(剖面/体)异步加载开始 → 列表项复选框转等待 spinner;渲染完成 → 复原复选框。
|
||||||
// 覆盖非三维体:勾选剖面首次渲染较慢时也有等待反馈(用户反馈)。
|
// 覆盖非三维体:勾选剖面首次渲染较慢时也有等待反馈(用户反馈)。
|
||||||
QObject::connect(sceneCtrl, &geopro::controller::VtkSceneController::datasetLoading, analysisTab,
|
QObject::connect(sceneCtrl, &geopro::controller::VtkSceneController::datasetLoading, analysisTab,
|
||||||
|
|
@ -1345,10 +1263,26 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
sceneView->setAnomalyVisible(id.toStdString(), vis);
|
sceneView->setAnomalyVisible(id.toStdString(), vis);
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
});
|
});
|
||||||
|
// 贴合坐标轴(T2):据非空选中 ds 解析其子树盒并显示贴合轴。返回 true=已应用贴合轴(有效盒)。
|
||||||
|
// 树选中与 VTK 视口点选共用同一解析(子树 dsId → datasetBounds → showFittedAxes),保证两路对称。
|
||||||
|
// 调用方据返回值决定退回策略(树路:非空但无盒 → 全景轴;视口路:无盒 → 保持现状不强推全景)。
|
||||||
|
auto applyFittedAxes = [sceneView, analysisTab](const QString& dsId) -> bool {
|
||||||
|
if (dsId.isEmpty()) return false;
|
||||||
|
const QStringList sub = analysisTab->subtreeDsIds(dsId);
|
||||||
|
std::vector<std::string> ids;
|
||||||
|
ids.reserve(static_cast<size_t>(sub.size()));
|
||||||
|
for (const QString& s : sub) ids.push_back(s.toStdString());
|
||||||
|
double box[6];
|
||||||
|
if (!ids.empty() && sceneView->datasetBounds(ids, box)) {
|
||||||
|
sceneView->showFittedAxes(box);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
// 树选中切片/异常 → VTK 高亮联动(正向 list→VTK;反向 VTK→list 需拾取回调,见 OPT-002)。
|
// 树选中切片/异常 → VTK 高亮联动(正向 list→VTK;反向 VTK→list 需拾取回调,见 OPT-002)。
|
||||||
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::datasetSelected, vtkWidget,
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::datasetSelected, vtkWidget,
|
||||||
[sceneView, interactionMgr, renderWindowPtr](const QString& dsId,
|
[sceneView, interactionMgr, renderWindowPtr, applyFittedAxes](
|
||||||
const QString& ddCode) {
|
const QString& dsId, const QString& ddCode) {
|
||||||
const std::string id = dsId.toStdString();
|
const std::string id = dsId.toStdString();
|
||||||
// 选中项决定高亮:异常↔切片互斥,选其它对象两者都清(否则切到别的对象后切片/异常仍高亮)。
|
// 选中项决定高亮:异常↔切片互斥,选其它对象两者都清(否则切到别的对象后切片/异常仍高亮)。
|
||||||
if (ddCode == QStringLiteral("dd_anomaly")) {
|
if (ddCode == QStringLiteral("dd_anomaly")) {
|
||||||
|
|
@ -1361,17 +1295,52 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
sceneView->setSelectedAnomaly(std::string{});
|
sceneView->setSelectedAnomaly(std::string{});
|
||||||
interactionMgr->deselectSlice();
|
interactionMgr->deselectSlice();
|
||||||
}
|
}
|
||||||
|
// 贴合坐标轴(T2):选中 → 该 ds 子树盒贴合轴+隐全景;空选中(取消) → 恢复全景轴。
|
||||||
|
// 子树未渲染/无盒 → 退回全景轴,避免留下无据的贴合框。
|
||||||
|
if (!applyFittedAxes(dsId)) sceneView->showSceneAxes();
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
});
|
});
|
||||||
|
// 双击 DS(决策6/T4):(a) 相机适配到该 ds 子树空间范围(复用 T2 subtreeDsIds + T1
|
||||||
|
// datasetBounds/fitToBounds,与选中贴合轴同一子树盒);(b) 有详情页的类型联动打开中下方详情面板,
|
||||||
|
// 三维体等无详情页类型静默——gate 在联动入口(detailCtrl.supports),不走 openDataset→loadFailed
|
||||||
|
// 的状态栏提示。属性弹窗改由右键「详情」(detailRequested) 触发,双击不再弹属性框。
|
||||||
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::datasetActivated, &window,
|
||||||
|
[sceneView, renderWindowPtr, analysisTab, &detailCtrl](
|
||||||
|
const QString& dsId, const QString& ddCode, const QString& name) {
|
||||||
|
const QStringList sub = analysisTab->subtreeDsIds(dsId);
|
||||||
|
std::vector<std::string> ids;
|
||||||
|
ids.reserve(static_cast<size_t>(sub.size()));
|
||||||
|
for (const QString& s : sub) ids.push_back(s.toStdString());
|
||||||
|
double box[6];
|
||||||
|
if (!ids.empty() && sceneView->datasetBounds(ids, box)) { // 未渲染/无盒 → 跳过适配(静默)
|
||||||
|
sceneView->fitToBounds(box);
|
||||||
|
renderWindowPtr->Render();
|
||||||
|
}
|
||||||
|
if (detailCtrl.supports(ddCode)) // 无详情页策略(三维体等)→ 只适配、不开面板(静默)
|
||||||
|
detailCtrl.openDataset(dsId, ddCode, name, QString());
|
||||||
|
});
|
||||||
|
// 2D 段「z 值」滑块 → 整体升降该 2D 类型平面(Plane2DRenderStrategy 重摆其全部足迹)。
|
||||||
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::planeZChanged, sceneCtrl,
|
||||||
|
&geopro::controller::VtkSceneController::setPlaneZ);
|
||||||
|
// 2D 段「底图」弹窗 → 切该类型平面底图 矢量平面/无 + 透明度(Plane2DRenderStrategy 多实例底图)。
|
||||||
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::basemapKindChanged, sceneCtrl,
|
||||||
|
&geopro::controller::VtkSceneController::setBasemapKind);
|
||||||
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::basemapOpacityChanged, sceneCtrl,
|
||||||
|
&geopro::controller::VtkSceneController::setBasemapOpacity);
|
||||||
// 反向 VTK→list:在 VTK 里点中/选中一张切片 → 在三维体段树里同步选中该切片行(②反向)。
|
// 反向 VTK→list:在 VTK 里点中/选中一张切片 → 在三维体段树里同步选中该切片行(②反向)。
|
||||||
// 点空白/清选(dsId 空) → 一并清 VTK 异常高亮(否则取消选中后异常图形仍高亮,用户反馈)。
|
// 点空白/清选(dsId 空) → 一并清 VTK 异常高亮(否则取消选中后异常图形仍高亮,用户反馈)。
|
||||||
interactionMgr->onSliceSelectionChanged = [drawer, sceneView, renderWindowPtr](
|
interactionMgr->onSliceSelectionChanged = [drawer, sceneView, renderWindowPtr, applyFittedAxes](
|
||||||
const std::string& dsId) {
|
const std::string& dsId) {
|
||||||
if (auto* sec = drawer->analysisTab()->section("voxel"))
|
if (auto* sec = drawer->analysisTab()->section("voxel"))
|
||||||
sec->selectItem(QString::fromStdString(dsId));
|
sec->selectItem(QString::fromStdString(dsId));
|
||||||
if (dsId.empty()) {
|
if (dsId.empty()) {
|
||||||
sceneView->setSelectedAnomaly(std::string{});
|
sceneView->setSelectedAnomaly(std::string{});
|
||||||
|
sceneView->showSceneAxes(); // VTK 里点空白清选 → 一并恢复全景轴(selectItem 空被 blocker 拦,不走 datasetSelected)
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
|
} else {
|
||||||
|
// 视口内点选切片/异常/体 → 与树选一致显示子树贴合轴。selectItem 在 QSignalBlocker 下不发
|
||||||
|
// datasetSelected(防选择环),故这里直接走共用解析补上贴合轴。无盒则保持现状(不强推全景)。
|
||||||
|
if (applyFittedAxes(QString::fromStdString(dsId))) renderWindowPtr->Render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 已保存切片经 VTK 右键「关闭」→ 取消数据列表对应切片项的勾选(否则列表仍勾选,用户反馈)。
|
// 已保存切片经 VTK 右键「关闭」→ 取消数据列表对应切片项的勾选(否则列表仍勾选,用户反馈)。
|
||||||
|
|
@ -1385,48 +1354,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 当前底图选择(默认 天地图=Satellite,对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。
|
// 当前底图选择(默认 天地图=Satellite,对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。
|
||||||
auto basemapKind =
|
auto basemapKind =
|
||||||
std::make_shared<geopro::app::TileBasemap::Kind>(geopro::app::TileBasemap::Satellite);
|
std::make_shared<geopro::app::TileBasemap::Kind>(geopro::app::TileBasemap::Satellite);
|
||||||
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::basemapChanged, basemap,
|
// B2:col2D 已删 —— 旧 2D 面板信号(basemapChanged / checkedDatasetsChanged / view2DModeChanged /
|
||||||
[basemap, basemapKind](int idx) {
|
// customZChanged)接线随之移除。其替代(工具条 3D 底图、平面 z 滑块、2D 底图弹层)由 Phase D3/E3/F2
|
||||||
// 地图下拉:0 天地图(卫星影像) / 1 Google(暂未实现→隐藏) / 2 隐藏。
|
// 重接。轨迹(足迹)勾选现经统一段 checkedDatasetsChanged → pushChecked → "plane2d" 策略渲染。
|
||||||
*basemapKind = (idx == 0) ? geopro::app::TileBasemap::Satellite
|
// 底图默认仍为天地图(basemapKind=Satellite),首个数据重锚后由 onFrameReanchored 显示。
|
||||||
: geopro::app::TileBasemap::Hidden;
|
// 抽屉去 tab 后无「三维/二维分析」切换,视图固定三维分析;旧 analysisModeChanged 接线移除(C2/D3 重接)。
|
||||||
basemap->show(*basemapKind);
|
|
||||||
});
|
|
||||||
// ── 二维数据集栏:勾选足迹(测线/轨迹) → 平铺进 View3D 地图;2D视图下拉控摆放高度 ──
|
|
||||||
// 足迹经控制器 loadMapLine(Api3dRepository 走 dd/ert/trajectory/line 端点) → addMapLine 至
|
|
||||||
// 当前摆放 Z,与帘面/底图共享 GeoLocalFrame 配准。与 3D 勾选集独立、按 dsId 增量。
|
|
||||||
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::checkedDatasetsChanged,
|
|
||||||
sceneCtrl, &geopro::controller::VtkSceneController::setChecked2DDatasets);
|
|
||||||
// 2D视图下拉(0关闭/1 Z=0/2顶部/3底部/4自定义) + 自定义 Z → 控制器重摆已勾选足迹。
|
|
||||||
auto custom2dZ = std::make_shared<double>(0.0);
|
|
||||||
// 默认 1(Z=0):与「2D视图」下拉可见默认项(setCurrentIndex(1))及控制器默认摆放一致——
|
|
||||||
// 组合框初始 view2DModeChanged 因 connect 前发射而丢失,此处不可依赖其同步初值。
|
|
||||||
auto view2dMode = std::make_shared<int>(1);
|
|
||||||
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::view2DModeChanged, sceneCtrl,
|
|
||||||
[sceneCtrl, custom2dZ, view2dMode](int mode) {
|
|
||||||
*view2dMode = mode;
|
|
||||||
sceneCtrl->set2DPlacement(mode, *custom2dZ);
|
|
||||||
});
|
|
||||||
// 自定义 Z 变化:记录;若当前正处自定义模式则即时重摆(控制器内 changed 判定避免无谓重画)。
|
|
||||||
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::customZChanged, sceneCtrl,
|
|
||||||
[sceneCtrl, custom2dZ, view2dMode](double z) {
|
|
||||||
*custom2dZ = z;
|
|
||||||
if (*view2dMode == 4) sceneCtrl->set2DPlacement(4, z);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── 二维分析改造 A 期:切「三维分析/二维分析」tab → 一场景两相机 ──────────────────
|
|
||||||
// 三处协作:①切片隐藏+交互锁(仅平移+缩放) [InteractionManager];②按目标维度重置取景基线
|
|
||||||
// [VtkSceneController]——使切换后该维度首条数据自动取景;③维度显隐+近俯视/自由相机+取景+坐标轴+
|
|
||||||
// 渲染 [VtkSceneView]。顺序:先 ①②(都不渲染),最后 ③ 收尾统一渲染。只翻可见标志、不清空/重建 →
|
|
||||||
// 切换瞬时;地形+底图常驻。
|
|
||||||
QObject::connect(drawer, &geopro::app::ColumnDrawer::analysisModeChanged, &window,
|
|
||||||
[interactionMgr, sceneCtrl, sceneView, viewToolbar, refreshAlongLineBar](bool is2D) {
|
|
||||||
interactionMgr->setMode2D(is2D);
|
|
||||||
sceneCtrl->onAnalysisModeChanged(is2D);
|
|
||||||
sceneView->setAnalysisMode2D(is2D);
|
|
||||||
viewToolbar->setAnalysisMode2D(is2D); // 二维下禁用 6 向快捷视图
|
|
||||||
(*refreshAlongLineBar)(); // 二维隐藏沿线滑块、三维细长体显示(B#2)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
|
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
|
||||||
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
|
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
|
||||||
|
|
@ -1435,8 +1367,27 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
};
|
};
|
||||||
// 相机程序化变化(取景/预设/缩放)后,底图按新视锥重算覆盖(治首帧部分瓦片需手动微动才出)。
|
// 相机程序化变化(取景/预设/缩放)后,底图按新视锥重算覆盖(治首帧部分瓦片需手动微动才出)。
|
||||||
sceneView->onCameraChanged = [basemap]() { basemap->refresh(); };
|
sceneView->onCameraChanged = [basemap]() { basemap->refresh(); };
|
||||||
|
// D3:渲染区工具条「底图」控件 → 共享 3D 底图(天地图/无 + 透明度),替代旧 col2D basemapChanged 接线。
|
||||||
|
QObject::connect(viewToolbar, &geopro::app::VtkViewToolbar::basemapKindChanged, basemap,
|
||||||
|
[basemap, basemapKind](int kind) {
|
||||||
|
*basemapKind = (kind == 0) ? geopro::app::TileBasemap::Satellite
|
||||||
|
: geopro::app::TileBasemap::Hidden;
|
||||||
|
basemap->show(*basemapKind);
|
||||||
|
});
|
||||||
|
QObject::connect(viewToolbar, &geopro::app::VtkViewToolbar::basemapOpacityChanged, basemap,
|
||||||
|
[basemap](double o) { basemap->setOpacity(o); });
|
||||||
// 底图最大范围按当前勾选剖面合并范围动态定(随增删自动伸缩);刷新时实时查询。
|
// 底图最大范围按当前勾选剖面合并范围动态定(随增删自动伸缩);刷新时实时查询。
|
||||||
basemap->setDataRadiusProvider([sceneView]() { return sceneView->dataHorizontalRadius(); });
|
basemap->setDataRadiusProvider([sceneView]() { return sceneView->dataHorizontalRadius(); });
|
||||||
|
// F2:2D 平面底图工厂下发 → Plane2DRenderStrategy 据此为每个 2D 类型平面按需建/销平面矢量底图
|
||||||
|
// (与共享 3D 底图同源 scene/渲染窗/frame/数据半径规则;随平面 z 升降重建、全消随平面消失)。
|
||||||
|
// 工厂造 TileBasemapPlaneAdapter(app 层适配 IPlaneBasemap),使 geopro_controller 不反依赖 app/VTK。
|
||||||
|
sceneCtrl->setPlaneBasemapFactory(
|
||||||
|
[scene, renderWindowPtr, frame, sceneView](double groundZ)
|
||||||
|
-> std::unique_ptr<geopro::controller::IPlaneBasemap> {
|
||||||
|
return std::make_unique<geopro::app::TileBasemapPlaneAdapter>(
|
||||||
|
*scene, renderWindowPtr, frame, groundZ,
|
||||||
|
[sceneView]() { return sceneView->dataHorizontalRadius(); });
|
||||||
|
});
|
||||||
// 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。单一来源 kVerticalExaggeration 下发
|
// 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。单一来源 kVerticalExaggeration 下发
|
||||||
// 控制器(上方)/底图;运行时 VE 改由坐标轴设置抽屉「应用」下发(旧三维数据集栏滑块已退役)。
|
// 控制器(上方)/底图;运行时 VE 改由坐标轴设置抽屉「应用」下发(旧三维数据集栏滑块已退役)。
|
||||||
basemap->setVerticalExaggeration(kVerticalExaggeration);
|
basemap->setVerticalExaggeration(kVerticalExaggeration);
|
||||||
|
|
@ -1517,7 +1468,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
|
|
||||||
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
|
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
|
||||||
// 引导层定位后,把工具条与提示浮层 raise 到其上 → 工具条永在最上层(修:缩小渲染区时引导层挡工具条)。
|
// 引导层定位后,把工具条与提示浮层 raise 到其上 → 工具条永在最上层(修:缩小渲染区时引导层挡工具条)。
|
||||||
emptyCentering->setRaiseAfter({viewToolbar, anomalyHint, elevHint, alongLineBar});
|
emptyCentering->setRaiseAfter({viewToolbar, anomalyHint, alongLineBar});
|
||||||
emptyCentering->reposition();
|
emptyCentering->reposition();
|
||||||
// 引导层隐藏器就位(见 pushChecked 处声明):场景(剖面∪三维分析)有勾选 → 隐藏不透明引导层、露出渲染。
|
// 引导层隐藏器就位(见 pushChecked 处声明):场景(剖面∪三维分析)有勾选 → 隐藏不透明引导层、露出渲染。
|
||||||
*setSceneEmptyVisible = [emptyState](bool empty) { emptyState->setVisible(empty); };
|
*setSceneEmptyVisible = [emptyState](bool empty) { emptyState->setVisible(empty); };
|
||||||
|
|
@ -1701,7 +1652,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
emptyState->setVisible(sources.isEmpty() && checkedAnalysis->isEmpty());
|
emptyState->setVisible(sources.isEmpty() && checkedAnalysis->isEmpty());
|
||||||
if (sources.isEmpty()) {
|
if (sources.isEmpty()) {
|
||||||
*lastSourceRows = {};
|
*lastSourceRows = {};
|
||||||
refreshAnalysis(); // 清空 5 段(客户端三维体仍驻留) + col2D
|
refreshAnalysis(); // 清空各段(客户端三维体仍驻留)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 多源异步汇总:每个源(TM / GS·项目根直挂)按 confType 取整棵 ds 子树,全部回来后 splitByCategory 分 5 段。
|
// 多源异步汇总:每个源(TM / GS·项目根直挂)按 confType 取整棵 ds 子树,全部回来后 splitByCategory 分 5 段。
|
||||||
|
|
@ -1710,7 +1661,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto finish = [acc, generation, myGen, lastSourceRows, refreshAnalysis]() {
|
auto finish = [acc, generation, myGen, lastSourceRows, refreshAnalysis]() {
|
||||||
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
|
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
|
||||||
*lastSourceRows = *acc; // 全部对象树 ds 作分析数据源
|
*lastSourceRows = *acc; // 全部对象树 ds 作分析数据源
|
||||||
refreshAnalysis(); // splitByCategory→5段 + 合并三维体/切片 + dim2D→col2D
|
refreshAnalysis(); // splitByCategory→各段 + 合并三维体/切片(单列)
|
||||||
};
|
};
|
||||||
for (const geopro::data::DataSource& src : sources) {
|
for (const geopro::data::DataSource& src : sources) {
|
||||||
// 第3参 confType:1=GS/项目根(直挂 ds),2=TM(测线下 ds)——透传给 loadRowsAsync(spec §6)。
|
// 第3参 confType:1=GS/项目根(直挂 ds),2=TM(测线下 ds)——透传给 loadRowsAsync(spec §6)。
|
||||||
|
|
@ -1800,23 +1751,82 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
centralStack->setCurrentWidget(dockManager);
|
centralStack->setCurrentWidget(dockManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 设备菜单「导入雷达测线」(后端未就绪的过渡测试入口):原入口在三维体段头按钮(已移除),集中到设备菜单。
|
||||||
|
// impulse=false 走规范化(.head/.data, 懒加载后台建体);true 走 Impulse(.iprb, eager)。导入流程目标不变。
|
||||||
|
QObject::connect(
|
||||||
|
topBar, &geopro::app::TopBar::radarImportRequested, &window,
|
||||||
|
[&window, scene3dRepo, refreshAnalysis, analysisTab, vtkLoading](bool impulse) {
|
||||||
|
if (!impulse) { // 规范化 .head/.data → registerRadarDataset(dd_radar_3d, 懒加载后台建体)
|
||||||
|
const QString dir = QFileDialog::getExistingDirectory(
|
||||||
|
&window, QStringLiteral("选择规范化三维雷达测线目录(含 *.head/*.data)"));
|
||||||
|
if (dir.isEmpty()) return;
|
||||||
|
bool ok = false;
|
||||||
|
const QString prefix = QInputDialog::getText(
|
||||||
|
&window, QStringLiteral("测线前缀"),
|
||||||
|
QStringLiteral("输入测线前缀(如 南同大道_000):"), QLineEdit::Normal, QString(), &ok);
|
||||||
|
if (!ok || prefix.isEmpty()) return;
|
||||||
|
// structParentId 暂空(P0 挂三维体段根;P1 接 TM 归属)。
|
||||||
|
// coarse=1 全分辨率(沿线不抽稀):验收期要肉眼判读反射/双曲线/通道连续性,
|
||||||
|
// 不能被沿线抽稀糊掉。单线峰值内存 ~0.7–1.5GB(spec §8.4);若 OOM 退回 2。
|
||||||
|
const std::string newId = scene3dRepo->registerRadarDataset(
|
||||||
|
dir.toLocal8Bit().toStdString(), prefix.toLocal8Bit().toStdString(),
|
||||||
|
prefix.toStdString(), /*structParentId=*/std::string(), /*coarse=*/1);
|
||||||
|
{ const QSignalBlocker block(analysisTab); refreshAnalysis(); } // DS 进三维体段(不触发渲染)
|
||||||
|
const QString qid = QString::fromStdString(newId);
|
||||||
|
analysisTab->setItemChecked(qid, true); // 勾选 → addDatasetAsync → loadVolume 后台建体渲染
|
||||||
|
analysisTab->setItemBusy(qid, true); // spinner; 渲染完成由 datasetRendered 撤
|
||||||
|
analysisTab->scrollItemToTop(qid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 明星路 Impulse(.iprb):复用现成 createGprVolume(eager 同步建体,预填 cachedGrid)。双数据集互证下游几何无关。
|
||||||
|
const QString dir = QFileDialog::getExistingDirectory(
|
||||||
|
&window, QStringLiteral("选择 Impulse 测线目录(含 *.iprb/*.ord)"));
|
||||||
|
if (dir.isEmpty()) return;
|
||||||
|
bool ok = false;
|
||||||
|
const QString prefix = QInputDialog::getText(
|
||||||
|
&window, QStringLiteral("测线前缀"),
|
||||||
|
QStringLiteral("输入测线前缀(如 明星路_010):"), QLineEdit::Normal, QString(), &ok);
|
||||||
|
if (!ok || prefix.isEmpty()) return;
|
||||||
|
vtkLoading->showOver(QStringLiteral("正在建Impulse体…"));
|
||||||
|
// 内层捕获 window 引用(非 [=] 值拷贝):QMainWindow 拷贝构造已删除,且 showToast 需非 const QWidget*。
|
||||||
|
QTimer::singleShot(0, &window, [=, &window]() {
|
||||||
|
std::string newId;
|
||||||
|
try {
|
||||||
|
newId = scene3dRepo->createGprVolume(dir.toLocal8Bit().toStdString(),
|
||||||
|
prefix.toLocal8Bit().toStdString(),
|
||||||
|
prefix.toStdString(), /*coarse=*/8);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
vtkLoading->hide();
|
||||||
|
geopro::app::showToast(&window,
|
||||||
|
QStringLiteral("建体失败:%1").arg(QString::fromLocal8Bit(e.what())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{ const QSignalBlocker block(analysisTab); refreshAnalysis(); }
|
||||||
|
vtkLoading->hide();
|
||||||
|
const QString qid = QString::fromStdString(newId);
|
||||||
|
// createGprVolume 预填 cachedGrid → setItemChecked 内 loadVolume 同步渲染、datasetRendered 自动撤 busy;
|
||||||
|
// 故此处【不要】再 setItemBusy(true)(否则 spinner 永久转圈)。
|
||||||
|
analysisTab->setItemChecked(qid, true);
|
||||||
|
analysisTab->scrollItemToTop(qid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 切换到「不同项目」时先清空中央区,避免新项目残留旧项目的三栏数据与 VTK 渲染。
|
// 切换到「不同项目」时先清空中央区,避免新项目残留旧项目的三栏数据与 VTK 渲染。
|
||||||
// 仅真正换项目用(delete-refresh 等 switchProject(currentProjectId) 不走此处,避免误清)。
|
// 仅真正换项目用(delete-refresh 等 switchProject(currentProjectId) 不走此处,避免误清)。
|
||||||
auto clearCentral = [sceneCtrl, emptyState, checkedProfiles, checkedAnalysis,
|
auto clearCentral = [emptyState, checkedProfiles, checkedAnalysis,
|
||||||
pushChecked, lastSourceRows, refreshAnalysis, checkedSliceIds,
|
pushChecked, lastSourceRows, refreshAnalysis, checkedSliceIds,
|
||||||
syncSlices, basemap, sceneView, scene3dRepo]() {
|
syncSlices, basemap, sceneView, scene3dRepo]() {
|
||||||
// 切项目:先清内存态三维体/切片/异常(否则上个项目的产物残留进新项目列表),再 refreshAnalysis。
|
// 切项目:先清内存态三维体/切片/异常(否则上个项目的产物残留进新项目列表),再 refreshAnalysis。
|
||||||
scene3dRepo->clearMockData();
|
scene3dRepo->clearMockData();
|
||||||
// 数据源清空 → 5 段 + col2D 清空(refreshAnalysis 内 setBuckets/dim2D)。
|
// 数据源清空 → 各段清空(refreshAnalysis 内 setBuckets,含 trajectory 段)。
|
||||||
*lastSourceRows = {};
|
*lastSourceRows = {};
|
||||||
refreshAnalysis();
|
refreshAnalysis();
|
||||||
// 勾选集清空并下发空到 VTK(帘面/体素/切片/2D 足迹全部撤场)。
|
// 勾选集清空并下发空到 VTK(帘面/体素/切片/2D 足迹全部撤场——统一入口一并清)。
|
||||||
checkedProfiles->clear();
|
checkedProfiles->clear();
|
||||||
checkedAnalysis->clear();
|
checkedAnalysis->clear();
|
||||||
checkedSliceIds->clear();
|
checkedSliceIds->clear();
|
||||||
pushChecked(); // setCheckedDatasets({}) → 帘面/体素清空
|
pushChecked(); // setCheckedDatasets({}) → 帘面/体素/足迹清空(统一勾选集 diff 全移除)
|
||||||
syncSlices(); // 切片随空勾选调和
|
syncSlices(); // 切片随空勾选调和
|
||||||
sceneCtrl->setChecked2DDatasets({}); // 2D 足迹显式撤场(与 col2D 空勾选双保险)
|
|
||||||
// 复位重锚标志:增量勾选不走 clear(),不复位则旧标志残留 → 新项目数据不重锚 →
|
// 复位重锚标志:增量勾选不走 clear(),不复位则旧标志残留 → 新项目数据不重锚 →
|
||||||
// onFrameReanchored 不触发 → 下面 hide() 的底图永不再显。复位后新项目首个数据重锚→重显。
|
// onFrameReanchored 不触发 → 下面 hide() 的底图永不再显。复位后新项目首个数据重锚→重显。
|
||||||
sceneView->resetFrameAnchor();
|
sceneView->resetFrameAnchor();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "panels/columns/CategoryAnalysisTab.hpp"
|
#include "panels/columns/CategoryAnalysisTab.hpp"
|
||||||
|
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
|
#include <QLabel>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
|
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "panels/columns/CategorySection.hpp"
|
#include "panels/columns/CategorySection.hpp"
|
||||||
|
#include "repo/CategoryDescriptor.hpp" // categoryCatalog(含 trajectory 段)
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -33,23 +35,25 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
|
||||||
col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶
|
col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶
|
||||||
col->setSpacing(space::kSm);
|
col->setSpacing(space::kSm);
|
||||||
|
|
||||||
for (const CategorySpec& spec : categoryConfigs()) {
|
for (const auto& desc : geopro::data::categoryCatalog()) {
|
||||||
auto* sec = new CategorySection(spec, dict, content);
|
auto* sec = new CategorySection(desc, dict, content);
|
||||||
sections_[spec.id] = sec;
|
sections_[desc.id] = sec;
|
||||||
ordered_.push_back(sec);
|
ordered_.push_back(sec);
|
||||||
connect(sec, &CategorySection::collapsedChanged, this,
|
connect(sec, &CategorySection::collapsedChanged, this,
|
||||||
&CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch(向上收)
|
&CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch(向上收)
|
||||||
const std::string segId = spec.id;
|
const std::string segId = desc.id;
|
||||||
connect(sec, &CategorySection::checkedDatasetsChanged, this,
|
connect(sec, &CategorySection::checkedDatasetsChanged, this,
|
||||||
[this, segId](const QStringList& ids) {
|
[this, segId](const QStringList& ids) {
|
||||||
checkedBySeg_[segId] = ids;
|
checkedBySeg_[segId] = ids;
|
||||||
recomputeCheckedUnion();
|
recomputeCheckedUnion();
|
||||||
|
updatePlaceholderAndVisibility(); // 勾选/段内容变化后段「有无行」可能变 → 刷新显隐
|
||||||
});
|
});
|
||||||
connect(sec, &CategorySection::generateVolumeRequested, this,
|
connect(sec, &CategorySection::generateVolumeRequested, this,
|
||||||
&CategoryAnalysisTab::generateVolumeRequested);
|
&CategoryAnalysisTab::generateVolumeRequested);
|
||||||
connect(sec, &CategorySection::radarImportRequested, this,
|
// 注:「导入雷达测线」入口已迁至 TopBar「设备」菜单(Task D1);CategorySection 段头按钮与
|
||||||
&CategoryAnalysisTab::radarImportRequested);
|
// CategoryAnalysisTab::radarImportRequested 信号均已移除。
|
||||||
connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested);
|
connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested);
|
||||||
|
connect(sec, &CategorySection::datasetActivated, this, &CategoryAnalysisTab::datasetActivated);
|
||||||
connect(sec, &CategorySection::deleteDatasetRequested, this,
|
connect(sec, &CategorySection::deleteDatasetRequested, this,
|
||||||
&CategoryAnalysisTab::deleteDatasetRequested);
|
&CategoryAnalysisTab::deleteDatasetRequested);
|
||||||
connect(sec, &CategorySection::sliceRequested, this, &CategoryAnalysisTab::sliceRequested);
|
connect(sec, &CategorySection::sliceRequested, this, &CategoryAnalysisTab::sliceRequested);
|
||||||
|
|
@ -64,7 +68,24 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
|
||||||
&CategoryAnalysisTab::sliceExportDatRequested);
|
&CategoryAnalysisTab::sliceExportDatRequested);
|
||||||
connect(sec, &CategorySection::anomalyVisibilityChanged, this,
|
connect(sec, &CategorySection::anomalyVisibilityChanged, this,
|
||||||
&CategoryAnalysisTab::anomalyVisibilityChanged);
|
&CategoryAnalysisTab::anomalyVisibilityChanged);
|
||||||
connect(sec, &CategorySection::datasetSelected, this, &CategoryAnalysisTab::datasetSelected);
|
// 全列互斥选中:某段选中非空数据行 → 清其余各段选中,保证整列至多一个 ds 选中,
|
||||||
|
// 避免「电阻率段与三维体段各选一行」的二义(贴合轴该听谁)。clearSelection 内部 QSignalBlocker
|
||||||
|
// 阻断,被清段不回发 datasetSelected(空),无环路;inSelectionSync_ 为兜底防重入。
|
||||||
|
connect(sec, &CategorySection::datasetSelected, this,
|
||||||
|
[this, sec](const QString& dsId, const QString& ddCode) {
|
||||||
|
if (!dsId.isEmpty() && !inSelectionSync_) {
|
||||||
|
inSelectionSync_ = true;
|
||||||
|
for (auto* other : ordered_)
|
||||||
|
if (other != sec) other->clearSelection();
|
||||||
|
inSelectionSync_ = false;
|
||||||
|
}
|
||||||
|
emit datasetSelected(dsId, ddCode);
|
||||||
|
});
|
||||||
|
connect(sec, &CategorySection::planeZChanged, this, &CategoryAnalysisTab::planeZChanged);
|
||||||
|
connect(sec, &CategorySection::basemapKindChanged, this,
|
||||||
|
&CategoryAnalysisTab::basemapKindChanged);
|
||||||
|
connect(sec, &CategorySection::basemapOpacityChanged, this,
|
||||||
|
&CategoryAnalysisTab::basemapOpacityChanged);
|
||||||
// #7:各段等分 stretch → 内容都少时四段平分高度填满面板(初始与 VTK 区等高、不出滚动条);
|
// #7:各段等分 stretch → 内容都少时四段平分高度填满面板(初始与 VTK 区等高、不出滚动条);
|
||||||
// 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。
|
// 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。
|
||||||
col->addWidget(sec, 1);
|
col->addWidget(sec, 1);
|
||||||
|
|
@ -72,6 +93,18 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
|
||||||
// 尾部弹簧(末项):默认 0;全部段折叠时由 relayoutSections 置 1,吸收余量把段头顶到顶部。
|
// 尾部弹簧(末项):默认 0;全部段折叠时由 relayoutSections 置 1,吸收余量把段头顶到顶部。
|
||||||
col->addStretch(0);
|
col->addStretch(0);
|
||||||
scroll->setWidget(content);
|
scroll->setWidget(content);
|
||||||
|
|
||||||
|
// 全空占位:所有段都无可渲染数据行时显示,与 scroll 同级、互斥显隐(由 updatePlaceholderAndVisibility 切换)。
|
||||||
|
auto* placeholder = new QLabel(QStringLiteral("请在左侧对象树勾选测线 / 数据集"), this);
|
||||||
|
placeholder->setAlignment(Qt::AlignCenter);
|
||||||
|
placeholder->setWordWrap(true);
|
||||||
|
applyTokenizedStyleSheet(placeholder,
|
||||||
|
QStringLiteral("QLabel{color:{{text/tertiary}};padding:24px;}"));
|
||||||
|
outer->addWidget(placeholder, 0);
|
||||||
|
placeholder->hide();
|
||||||
|
placeholder_ = placeholder;
|
||||||
|
|
||||||
|
updatePlaceholderAndVisibility(); // 初始无数据 → 直接进入占位态
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategoryAnalysisTab::relayoutSections() {
|
void CategoryAnalysisTab::relayoutSections() {
|
||||||
|
|
@ -86,14 +119,29 @@ void CategoryAnalysisTab::relayoutSections() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
|
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
|
||||||
const auto& cfg = categoryConfigs();
|
// splitByCategory 现按 categoryCatalog() 分桶(含 trajectory 桶,自然分发到轨迹段)。
|
||||||
for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) {
|
// 轨迹段已随 catalog 在构造时建出(Task C2),section() 命中存在的段;下方 guard 仍保平衡兜底。
|
||||||
|
const auto& cat = geopro::data::categoryCatalog();
|
||||||
|
for (std::size_t i = 0; i < cat.size() && i < b.segments.size(); ++i) {
|
||||||
// voxel(三维体) 段数据来自 mock voxelTree(体/切片/异常),由调用方单独 section("voxel")->setDatasets
|
// voxel(三维体) 段数据来自 mock voxelTree(体/切片/异常),由调用方单独 section("voxel")->setDatasets
|
||||||
// 注入;splitByCategory 对它永远是空桶,若在此用空桶覆盖会先清掉其勾选(随后重填但勾选已丢) →
|
// 注入;splitByCategory 对它永远是空桶,若在此用空桶覆盖会先清掉其勾选(随后重填但勾选已丢) →
|
||||||
// 表现为「创建切片/异常后体/切片选择被清空」(用户 issue 1)。故跳过 voxel,勿覆盖。
|
// 表现为「创建切片/异常后体/切片选择被清空」(用户 issue 1)。故跳过 voxel,勿覆盖。
|
||||||
if (cfg[i].id == "voxel") continue;
|
if (cat[i].id == "voxel") continue;
|
||||||
if (auto* sec = section(cfg[i].id)) sec->setDatasets(b.segments[i]);
|
if (auto* sec = section(cat[i].id)) sec->setDatasets(b.segments[i]);
|
||||||
}
|
}
|
||||||
|
updatePlaceholderAndVisibility(); // 分发后据各段有无数据刷新显隐 + 占位
|
||||||
|
}
|
||||||
|
|
||||||
|
void CategoryAnalysisTab::updatePlaceholderAndVisibility() {
|
||||||
|
bool anyVisible = false;
|
||||||
|
for (auto* sec : ordered_) {
|
||||||
|
const bool has = sec->hasRenderableRows();
|
||||||
|
sec->setVisible(has);
|
||||||
|
if (has) anyVisible = true;
|
||||||
|
}
|
||||||
|
if (scroll_) scroll_->setVisible(anyVisible);
|
||||||
|
if (placeholder_) placeholder_->setVisible(!anyVisible);
|
||||||
|
relayoutSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategoryAnalysisTab::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
|
void CategoryAnalysisTab::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
|
||||||
|
|
@ -109,6 +157,14 @@ CategorySection* CategoryAnalysisTab::section(const std::string& id) const {
|
||||||
return it != sections_.end() ? it->second : nullptr;
|
return it != sections_.end() ? it->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList CategoryAnalysisTab::subtreeDsIds(const QString& dsId) const {
|
||||||
|
for (auto* sec : ordered_) {
|
||||||
|
const QStringList ids = sec->subtreeDsIds(dsId);
|
||||||
|
if (!ids.isEmpty()) return ids; // ds 归属唯一段 → 首个命中段即答案
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
// ds 归属唯一段未知 → 广播到各段,仅含该 ds 的段会命中生效(其余 no-op)。
|
// ds 归属唯一段未知 → 广播到各段,仅含该 ds 的段会命中生效(其余 no-op)。
|
||||||
void CategoryAnalysisTab::setItemChecked(const QString& dsId, bool on) {
|
void CategoryAnalysisTab::setItemChecked(const QString& dsId, bool on) {
|
||||||
for (auto* sec : ordered_) sec->setChecked(dsId, on);
|
for (auto* sec : ordered_) sec->setChecked(dsId, on);
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,24 @@ class CategoryAnalysisTab : public QWidget {
|
||||||
public:
|
public:
|
||||||
explicit CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* dict, QWidget* parent = nullptr);
|
explicit CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* dict, QWidget* parent = nullptr);
|
||||||
|
|
||||||
void setBuckets(const CategoryBuckets& b); // 分发到 5 段(与 categoryConfigs 同序)
|
void setBuckets(const CategoryBuckets& b); // 分发到各大类段(与 categoryCatalog() 同序)
|
||||||
void setStructure(const std::vector<geopro::data::StructNode>& nodes); // 转发各段
|
void setStructure(const std::vector<geopro::data::StructNode>& nodes); // 转发各段
|
||||||
void refreshArrayFilters(); // 装置枚举异步加载后,重填各段装置筛选下拉
|
void refreshArrayFilters(); // 装置枚举异步加载后,重填各段装置筛选下拉
|
||||||
CategorySection* section(const std::string& id) const; // 按 CategorySpec.id 取段
|
CategorySection* section(const std::string& id) const; // 按 CategoryDescriptor.id 取段
|
||||||
|
// 该 ds 所在段的层级子树 dsId 集(贴合坐标轴子树盒):遍历各段,返首个命中段的 subtreeDsIds。空=无段含该 ds。
|
||||||
|
QStringList subtreeDsIds(const QString& dsId) const;
|
||||||
// ── 三维体生成后:自动勾选触发渲染 + 标题前等待动画(spinner)反馈(转发到含该 ds 的段)──
|
// ── 三维体生成后:自动勾选触发渲染 + 标题前等待动画(spinner)反馈(转发到含该 ds 的段)──
|
||||||
void setItemChecked(const QString& dsId, bool on); // 自动勾选(触发渲染)
|
void setItemChecked(const QString& dsId, bool on); // 自动勾选(触发渲染)
|
||||||
void setItemBusy(const QString& dsId, bool busy); // 复选框↔等待 spinner 切换
|
void setItemBusy(const QString& dsId, bool busy); // 复选框↔等待 spinner 切换
|
||||||
void clearAllBusy(); // 撤回所有 spinner(失败兜底)
|
void clearAllBusy(); // 撤回所有 spinner(失败兜底)
|
||||||
void scrollItemToTop(const QString& dsId); // 把某行尽量滚到面板顶部(新建三维体后定位)
|
void scrollItemToTop(const QString& dsId); // 把某行尽量滚到面板顶部(新建三维体后定位)
|
||||||
|
void refreshVisibility() { updatePlaceholderAndVisibility(); } // 外部注入(如 voxel setDatasets)后刷新段显隐/占位
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void checkedDatasetsChanged(const QStringList& dsIds); // 5 段勾选并集
|
void checkedDatasetsChanged(const QStringList& dsIds); // 5 段勾选并集
|
||||||
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds);
|
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds);
|
||||||
void radarImportRequested(bool impulse); // 三维体段头「+导入雷达测线」(false=规范化, true=Impulse)
|
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 右键「详情」=属性弹窗
|
||||||
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name);
|
void datasetActivated(const QString& dsId, const QString& ddCode, const QString& name); // 双击=适配+图表联动(T4)
|
||||||
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除切片/异常
|
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除切片/异常
|
||||||
// ── 三维体段操作转发(迁自旧 Column3DAnalysis,全接)──
|
// ── 三维体段操作转发(迁自旧 Column3DAnalysis,全接)──
|
||||||
void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId);
|
void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId);
|
||||||
|
|
@ -52,6 +55,9 @@ signals:
|
||||||
void sliceExportDatRequested(const QString& dsId);
|
void sliceExportDatRequested(const QString& dsId);
|
||||||
void anomalyVisibilityChanged(const QString& dsId, bool vis);
|
void anomalyVisibilityChanged(const QString& dsId, bool vis);
|
||||||
void datasetSelected(const QString& dsId, const QString& ddCode); // 树选中→VTK 高亮联动
|
void datasetSelected(const QString& dsId, const QString& ddCode); // 树选中→VTK 高亮联动
|
||||||
|
void planeZChanged(const QString& typeId, double z); // 2D 段 z 值滑块:整体升降该类型平面
|
||||||
|
void basemapKindChanged(const QString& typeId, int kind); // 2D 段底图弹窗:矢量平面(0)/无(1)
|
||||||
|
void basemapOpacityChanged(const QString& typeId, double o); // 2D 段底图弹窗:透明度[0,1]
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void recomputeCheckedUnion();
|
void recomputeCheckedUnion();
|
||||||
|
|
@ -61,13 +67,17 @@ private:
|
||||||
// 据各段折叠态重排 stretch:折叠段=0(仅段头高)、展开段=1(吸收余量);全折叠时尾部弹簧吸收→段头顶到顶部。
|
// 据各段折叠态重排 stretch:折叠段=0(仅段头高)、展开段=1(吸收余量);全折叠时尾部弹簧吸收→段头顶到顶部。
|
||||||
// 仅在面板不产生滚动条(内容short于视口)时有可见效果——正是用户反馈的"折叠后停在原位中间"场景。
|
// 仅在面板不产生滚动条(内容short于视口)时有可见效果——正是用户反馈的"折叠后停在原位中间"场景。
|
||||||
void relayoutSections();
|
void relayoutSections();
|
||||||
|
// 据各段是否有可渲染数据行 显隐段;全空时显示占位提示、隐藏滚动区。数据变化(分发/勾选/注入)后调用。
|
||||||
|
void updatePlaceholderAndVisibility();
|
||||||
|
|
||||||
std::map<std::string, CategorySection*> sections_;
|
std::map<std::string, CategorySection*> sections_;
|
||||||
std::vector<CategorySection*> ordered_; // 按 categoryConfigs 顺序(relayout 遍历用)
|
std::vector<CategorySection*> ordered_; // 按 categoryCatalog() 顺序(relayout 遍历用)
|
||||||
QScrollArea* scroll_ = nullptr; // 外层滚动区(scrollItemToTop 定位用)
|
QScrollArea* scroll_ = nullptr; // 外层滚动区(scrollItemToTop 定位用)
|
||||||
QWidget* content_ = nullptr; // 滚动内容容器(坐标映射用)
|
QWidget* content_ = nullptr; // 滚动内容容器(坐标映射用)
|
||||||
QVBoxLayout* col_ = nullptr; // 段堆叠布局(末项=尾部弹簧)
|
QVBoxLayout* col_ = nullptr; // 段堆叠布局(末项=尾部弹簧)
|
||||||
|
QWidget* placeholder_ = nullptr; // 全空时显示的占位提示(与 scroll_ 同级,互斥显隐)
|
||||||
std::map<std::string, QStringList> checkedBySeg_; // 各段当前勾选(合并成并集)
|
std::map<std::string, QStringList> checkedBySeg_; // 各段当前勾选(合并成并集)
|
||||||
|
bool inSelectionSync_ = false; // 跨段互斥清选进行中标记(防重入信号环,clearSelection 已阻断信号,此为兜底)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,23 @@
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QMouseEvent>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
#include <QSlider>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
|
#include <QWidgetAction>
|
||||||
#include <QTreeWidgetItemIterator>
|
#include <QTreeWidgetItemIterator>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "panels/DatasetListPanel.hpp"
|
#include "panels/DatasetListPanel.hpp"
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
#include "repo/DatasetFieldDictionary.hpp"
|
#include "repo/DatasetFieldDictionary.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -27,14 +32,20 @@ namespace geopro::app {
|
||||||
using geopro::data::DsRow;
|
using geopro::data::DsRow;
|
||||||
using geopro::data::DsTypeFields;
|
using geopro::data::DsTypeFields;
|
||||||
|
|
||||||
CategorySection::CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict,
|
namespace {
|
||||||
QWidget* parent)
|
// 段头图标条默认上限(spec §6):超过则末位收进「…」下拉。钉死为常量而非随操作数浮动,
|
||||||
: QWidget(parent), spec_(spec), dict_(dict) {
|
// 否则上限恒=操作数、计数溢出折叠分支永不触发(当前各段操作 ≤3,对用户不可见,但保证分支正确)。
|
||||||
|
constexpr int kDefaultMaxIcons = 3;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc,
|
||||||
|
geopro::data::DatasetFieldDictionary* dict, QWidget* parent)
|
||||||
|
: QWidget(parent), desc_(desc), dict_(dict) {
|
||||||
auto* root = new QVBoxLayout(this);
|
auto* root = new QVBoxLayout(this);
|
||||||
root->setContentsMargins(0, 0, 0, 0);
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
root->setSpacing(0);
|
root->setSpacing(0);
|
||||||
|
|
||||||
// 数据类型段头(可折叠,规范§4.3/§6):chevron + 标题(title 字号·半粗) |「+ 新增三维体」(右,仅反演类)。
|
// 数据类型段头(可折叠,规范§4.3/§6):chevron + 标题(title 字号·半粗) | 右侧响应式图标条(SectionIconBar)。
|
||||||
// 段头加浅底 + 底分隔线作视觉分段;去原生小三角(难看)→chevron 文本前缀,随主题/hover 变色。
|
// 段头加浅底 + 底分隔线作视觉分段;去原生小三角(难看)→chevron 文本前缀,随主题/hover 变色。
|
||||||
auto* headerRow = new QWidget(this);
|
auto* headerRow = new QWidget(this);
|
||||||
headerRow->setObjectName(QStringLiteral("secHeader"));
|
headerRow->setObjectName(QStringLiteral("secHeader"));
|
||||||
|
|
@ -58,57 +69,44 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
.arg(type::kWeightSemibold));
|
.arg(type::kWeightSemibold));
|
||||||
auto syncHeader = [this] {
|
auto syncHeader = [this] {
|
||||||
header_->setText((header_->isChecked() ? QStringLiteral("▾ ") : QStringLiteral("▸ "))
|
header_->setText((header_->isChecked() ? QStringLiteral("▾ ") : QStringLiteral("▸ "))
|
||||||
+ QString::fromStdString(spec_.title));
|
+ QString::fromStdString(desc_.title));
|
||||||
};
|
};
|
||||||
syncHeader();
|
syncHeader();
|
||||||
|
// 标题先让位:水平 Preferred + 最小宽 0,列变窄时标题先收(必要时裁字),图标条守住自身
|
||||||
|
// sizeHint(全图标宽)直到真正没空间才折叠右侧图标进「…」。否则标题不肯缩→图标条被迫先折(过早)。
|
||||||
|
header_->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||||
|
header_->setMinimumWidth(0);
|
||||||
hl->addWidget(header_);
|
hl->addWidget(header_);
|
||||||
hl->addStretch(1);
|
hl->addStretch(1);
|
||||||
if (spec_.canGenerateVolume) {
|
// 段头图标条:遍历 desc_.operations,经一处 OpKind→IconAction 映射装配(spec §6)。
|
||||||
auto* gen = new QToolButton(headerRow);
|
// glyph 键须命中 SectionIconBar::glyphFromKey 已识别集;z 值无专用键,复用 collapse(竖向双箭头)。
|
||||||
gen->setText(QStringLiteral("+ 新增三维体"));
|
iconBar_ = new SectionIconBar(headerRow);
|
||||||
gen->setCursor(Qt::PointingHandCursor);
|
std::vector<IconAction> acts;
|
||||||
// 次级强调按钮(规范§6.7):描边 accent + accent 文字,hover 浅强调底;非裸文字。
|
for (geopro::data::OpKind op : desc_.operations) {
|
||||||
applyTokenizedStyleSheet(
|
switch (op) {
|
||||||
gen, QStringLiteral(
|
case geopro::data::OpKind::GenerateVolume:
|
||||||
"QToolButton{border:1px solid {{accent/primary}};border-radius:%1px;"
|
// dsTypeCode 不再由段配置带(描述符无此字段)→ 发空串,接收方按 sourceIds 解析类型。
|
||||||
"color:{{accent/primary}};background:transparent;padding:%2px %3px;font-size:%4px;}"
|
acts.push_back({QStringLiteral("plus"), QStringLiteral("新增三维体"),
|
||||||
"QToolButton:hover{background:{{bg/selected}};}"
|
[this] { emit generateVolumeRequested(QString(), checkedDsIds()); }, {}});
|
||||||
"QToolButton:pressed{background:{{bg/hover}};}")
|
break;
|
||||||
.arg(radius::kSm)
|
case geopro::data::OpKind::Filter:
|
||||||
.arg(scaledPx(space::kXxs))
|
acts.push_back({QStringLiteral("filter"), QStringLiteral("筛选"),
|
||||||
.arg(scaledPx(space::kMd))
|
[this] { if (filterRow_) filterRow_->setVisible(!filterRow_->isVisible()); },
|
||||||
.arg(scaledPx(type::kCaption)));
|
{}});
|
||||||
connect(gen, &QToolButton::clicked, this, [this] {
|
break;
|
||||||
emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds());
|
case geopro::data::OpKind::PlaneZ:
|
||||||
});
|
acts.push_back({QStringLiteral("collapse"), QStringLiteral("z 值"), {},
|
||||||
hl->addWidget(gen);
|
[this](QToolButton* host) { showPlaneZPopup(host); }});
|
||||||
|
break;
|
||||||
|
case geopro::data::OpKind::Basemap:
|
||||||
|
acts.push_back({QStringLiteral("map"), QStringLiteral("底图"), {},
|
||||||
|
[this](QToolButton* host) { showBasemapPopup(host); }});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// 三维体段头「+ 导入雷达测线」(后端未就绪的本地过渡入口):弹出菜单选 规范化/Impulse。
|
|
||||||
// 次级强调按钮样式同「+新增三维体」;点击发 radarImportRequested(impulse) → 上层走导入流程。
|
|
||||||
if (spec_.id == "voxel") {
|
|
||||||
auto* imp = new QToolButton(headerRow);
|
|
||||||
imp->setText(QStringLiteral("+ 导入雷达测线"));
|
|
||||||
imp->setCursor(Qt::PointingHandCursor);
|
|
||||||
imp->setPopupMode(QToolButton::InstantPopup);
|
|
||||||
applyTokenizedStyleSheet(
|
|
||||||
imp, QStringLiteral(
|
|
||||||
"QToolButton{border:1px solid {{accent/primary}};border-radius:%1px;"
|
|
||||||
"color:{{accent/primary}};background:transparent;padding:%2px %3px;font-size:%4px;}"
|
|
||||||
"QToolButton::menu-indicator{image:none;width:0;}"
|
|
||||||
"QToolButton:hover{background:{{bg/selected}};}"
|
|
||||||
"QToolButton:pressed{background:{{bg/hover}};}")
|
|
||||||
.arg(radius::kSm)
|
|
||||||
.arg(scaledPx(space::kXxs))
|
|
||||||
.arg(scaledPx(space::kMd))
|
|
||||||
.arg(scaledPx(type::kCaption)));
|
|
||||||
auto* menu = new QMenu(imp);
|
|
||||||
menu->addAction(QStringLiteral("规范化测线目录(.head/.data)…"), this,
|
|
||||||
[this] { emit radarImportRequested(false); });
|
|
||||||
menu->addAction(QStringLiteral("Impulse 测线目录(.iprb)…"), this,
|
|
||||||
[this] { emit radarImportRequested(true); });
|
|
||||||
imp->setMenu(menu);
|
|
||||||
hl->addWidget(imp);
|
|
||||||
}
|
}
|
||||||
|
iconBar_->setMaxIcons(kDefaultMaxIcons); // spec §6 默认上限 3(够宽全显,超数/不够宽收进「…」)
|
||||||
|
iconBar_->setActions(acts);
|
||||||
|
hl->addWidget(iconBar_);
|
||||||
root->addWidget(headerRow);
|
root->addWidget(headerRow);
|
||||||
|
|
||||||
body_ = new QWidget(this);
|
body_ = new QWidget(this);
|
||||||
|
|
@ -116,20 +114,27 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd);
|
body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd);
|
||||||
body->setSpacing(space::kSm);
|
body->setSpacing(space::kSm);
|
||||||
|
|
||||||
// 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。
|
// 筛选行(默认折叠,由段头 Filter 图标 toggle):按 desc_.filters 装配 —— DateRange→采集时间范围(在前);
|
||||||
auto* filterRow = new QHBoxLayout();
|
// ArrayType→装置类型下拉(在后)。未列出的维度不建控件,passesFilters/rebuildList 视该控件缺席=不筛该维。
|
||||||
filterRow->setSpacing(space::kSm);
|
filterRow_ = new QWidget(body_);
|
||||||
dateRange_ = new DateRangeEdit(body_);
|
auto* filterLay = new QHBoxLayout(filterRow_);
|
||||||
|
filterLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
filterLay->setSpacing(space::kSm);
|
||||||
|
for (geopro::data::FilterKind fk : desc_.filters) {
|
||||||
|
if (fk == geopro::data::FilterKind::DateRange) {
|
||||||
|
dateRange_ = new DateRangeEdit(filterRow_);
|
||||||
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
|
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
|
||||||
filterRow->addWidget(dateRange_, 1);
|
filterLay->addWidget(dateRange_, 1);
|
||||||
if (spec_.hasArrayTypeFilter) {
|
} else if (fk == geopro::data::FilterKind::ArrayType) {
|
||||||
arrayCombo_ = new QComboBox(body_);
|
arrayCombo_ = new QComboBox(filterRow_);
|
||||||
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
|
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
|
||||||
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||||
[this](int) { rebuildList(); });
|
[this](int) { rebuildList(); });
|
||||||
filterRow->addWidget(arrayCombo_);
|
filterLay->addWidget(arrayCombo_);
|
||||||
}
|
}
|
||||||
body->addLayout(filterRow);
|
}
|
||||||
|
filterRow_->hide(); // 默认折叠,点段头筛选图标展开
|
||||||
|
body->addWidget(filterRow_);
|
||||||
|
|
||||||
// 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。
|
// 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。
|
||||||
list_ = new QTreeWidget(body_);
|
list_ = new QTreeWidget(body_);
|
||||||
|
|
@ -140,30 +145,52 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
list_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
list_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
list_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
list_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
applyDatasetCardDelegate(list_);
|
applyDatasetCardDelegate(list_);
|
||||||
|
list_->viewport()->installEventFilter(this); // 捕获按下瞬间选中态(默认改选前),供单击已选行 toggle 取消
|
||||||
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* it, int) {
|
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* it, int) {
|
||||||
|
// 用户点勾选框会在按下→释放间先发 itemChanged:标记本次按下改动了勾选框,itemClicked 据此不取消选中
|
||||||
|
// (点勾选框只切渲染,不应连带取消该行选中/丢贴合轴)。程序化改勾选均走 SignalBlocker,不到此处。
|
||||||
|
if (it && it == pressedItem_) checkToggledThisPress_ = true;
|
||||||
// 异常行复选框 = 该异常显隐(异常不进渲染勾选集,单独走 anomalyVisibilityChanged → setAnomalyVisible)。
|
// 异常行复选框 = 该异常显隐(异常不进渲染勾选集,单独走 anomalyVisibilityChanged → setAnomalyVisible)。
|
||||||
if (it && it->data(0, kDsDdCodeRole).toString() == QStringLiteral("dd_anomaly"))
|
if (it && it->data(0, kDsDdCodeRole).toString() == QStringLiteral("dd_anomaly"))
|
||||||
emit anomalyVisibilityChanged(it->data(0, kDsIdRole).toString(),
|
emit anomalyVisibilityChanged(it->data(0, kDsIdRole).toString(),
|
||||||
it->checkState(0) == Qt::Checked);
|
it->checkState(0) == Qt::Checked);
|
||||||
emitChecked();
|
emitChecked();
|
||||||
});
|
});
|
||||||
|
// 单击已选中的数据行 → 取消选中(toggle-off):itemClicked 于释放时触发,此时选中态已落定;
|
||||||
|
// 仅当「按下瞬间该行已选中」且「本次未点勾选框」才取消。取消走 list_->clearSelection()(非成员
|
||||||
|
// clearSelection,后者阻断信号)→ 直发 itemSelectionChanged(空) → datasetSelected("") → 恢复全景轴、丢贴合轴。
|
||||||
|
// 双击仍可激活:首次释放虽可能 toggle 掉选中,随后 DblClick 会重新选中该行并发 datasetActivated(fit+详情)。
|
||||||
|
connect(list_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* it, int) {
|
||||||
|
if (!it || it != pressedItem_ || !pressedSelected_ || checkToggledThisPress_) return;
|
||||||
|
if (it->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) return; // 容器行不参与选中
|
||||||
|
pressedSelected_ = false; // 防重入
|
||||||
|
list_->clearSelection(); // 发空 itemSelectionChanged → datasetSelected("") → 上层恢复全景轴
|
||||||
|
});
|
||||||
connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) {
|
connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) {
|
||||||
const QString id = it->data(0, kDsIdRole).toString();
|
const QString id = it->data(0, kDsIdRole).toString();
|
||||||
if (id.isEmpty()) return;
|
if (id.isEmpty()) return;
|
||||||
emit detailRequested(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString());
|
// 双击=适配相机到该 ds 子树盒 + 联动中下方图表详情页(无图表页类型静默,见 main.cpp T4 接线)。
|
||||||
|
// 属性弹窗改由右键「详情」触发(detailRequested),双击不再弹属性框(决策6:三维体只适配、静默)。
|
||||||
|
// 双击首击的 itemClicked 可能已把「本已选中」的该行 toggle 取消(见下方 toggle 逻辑);此处补回选中,
|
||||||
|
// 使双击终态 = 选中+贴合轴+详情一致(!isSelected 仅在被 toggle 掉时成立,正常双击未选行首击已选中→跳过)。
|
||||||
|
if (!it->isSelected()) list_->setCurrentItem(it);
|
||||||
|
emit datasetActivated(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString());
|
||||||
});
|
});
|
||||||
if (spec_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常)
|
// 树选中任意数据行 → datasetSelected(各类型段通用):驱动该 ds 子树贴合坐标轴(spec §3.2),
|
||||||
list_->setContextMenuPolicy(Qt::CustomContextMenu);
|
// 并由上层 CategoryAnalysisTab 做全列互斥。voxel 段切片/异常的 VTK 高亮由上层据 ddCode 处理,
|
||||||
connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu);
|
// 故此信号对所有段发射即可,任意类型选中都能显子树贴合轴(非仅三维体)。
|
||||||
// 树选中切片/异常 → VTK 高亮联动(正向 list→VTK)。
|
// 空选中(点树空白/清选)→ 发空 datasetSelected:上层据此恢复全景轴、清高亮(贴合轴取消)。
|
||||||
connect(list_, &QTreeWidget::itemSelectionChanged, this, [this] {
|
connect(list_, &QTreeWidget::itemSelectionChanged, this, [this] {
|
||||||
const auto items = list_->selectedItems();
|
const auto items = list_->selectedItems();
|
||||||
if (items.isEmpty()) return;
|
if (items.isEmpty()) { emit datasetSelected(QString(), QString()); return; }
|
||||||
QTreeWidgetItem* it = items.first();
|
QTreeWidgetItem* it = items.first();
|
||||||
const QString id = it->data(0, kDsIdRole).toString();
|
const QString id = it->data(0, kDsIdRole).toString();
|
||||||
const QString dd = it->data(0, kDsDdCodeRole).toString();
|
const QString dd = it->data(0, kDsDdCodeRole).toString();
|
||||||
if (!id.isEmpty() && dd != QStringLiteral("container")) emit datasetSelected(id, dd);
|
if (!id.isEmpty() && dd != QStringLiteral("container")) emit datasetSelected(id, dd);
|
||||||
});
|
});
|
||||||
|
if (desc_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常)
|
||||||
|
list_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu);
|
||||||
}
|
}
|
||||||
body->addWidget(list_, 1);
|
body->addWidget(list_, 1);
|
||||||
|
|
||||||
|
|
@ -176,6 +203,20 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CategorySection::eventFilter(QObject* obj, QEvent* ev) {
|
||||||
|
// 段体树 viewport 左键按下:在 QTreeWidget 默认处理改选之前,记录被按行及其「按下瞬间是否已选中」,
|
||||||
|
// 并清本次「勾选框改动」标记。itemClicked(释放)据此判定:已选中且未点勾选框 → toggle 取消选中。
|
||||||
|
if (list_ && obj == list_->viewport() && ev->type() == QEvent::MouseButtonPress) {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (me->button() == Qt::LeftButton) {
|
||||||
|
pressedItem_ = list_->itemAt(me->position().toPoint());
|
||||||
|
pressedSelected_ = pressedItem_ && pressedItem_->isSelected();
|
||||||
|
checkToggledThisPress_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(obj, ev);
|
||||||
|
}
|
||||||
|
|
||||||
bool CategorySection::isExpanded() const { return header_ && header_->isChecked(); }
|
bool CategorySection::isExpanded() const { return header_ && header_->isChecked(); }
|
||||||
|
|
||||||
void CategorySection::ensureExpanded() {
|
void CategorySection::ensureExpanded() {
|
||||||
|
|
@ -189,6 +230,37 @@ QTreeWidgetItem* CategorySection::itemFor(const QString& dsId) const {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList CategorySection::subtreeDsIds(const QString& dsId) const {
|
||||||
|
QTreeWidgetItem* item = itemFor(dsId);
|
||||||
|
if (!item) return {};
|
||||||
|
// 归一到子树根:向上找到最高的非容器祖先(三维体行;其父为结构容器 TM)。选中体/切片/异常都收敛到该体。
|
||||||
|
QTreeWidgetItem* root = item;
|
||||||
|
for (QTreeWidgetItem* p = root->parent(); p; p = p->parent()) {
|
||||||
|
if (p->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) break;
|
||||||
|
root = p;
|
||||||
|
}
|
||||||
|
// 自根向下遍历整棵子树,收集非空 dsId(跳过容器骨架节点)。
|
||||||
|
QStringList ids;
|
||||||
|
std::vector<QTreeWidgetItem*> stack{root};
|
||||||
|
while (!stack.empty()) {
|
||||||
|
QTreeWidgetItem* it = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
const QString id = it->data(0, kDsIdRole).toString();
|
||||||
|
if (!id.isEmpty()) ids << id;
|
||||||
|
for (int i = 0; i < it->childCount(); ++i) stack.push_back(it->child(i));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CategorySection::hasRenderableRows() const {
|
||||||
|
if (!list_) return false;
|
||||||
|
// 数据行 = 非 container 的可勾选行;只有容器节点(分组)不算「有数据」。
|
||||||
|
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||||||
|
if ((*it)->data(0, kDsDdCodeRole).toString() != QStringLiteral("container"))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void CategorySection::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
|
void CategorySection::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
|
||||||
structure_ = nodes; // 容器分层(项目根/GS/TM→ds)在 Task 12 接入真实结构后据此构建。
|
structure_ = nodes; // 容器分层(项目根/GS/TM→ds)在 Task 12 接入真实结构后据此构建。
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +281,13 @@ void CategorySection::selectItem(const QString& dsId) {
|
||||||
list_->setCurrentItem(nullptr); // 空 dsId / 未找到 → 清选中
|
list_->setCurrentItem(nullptr); // 空 dsId / 未找到 → 清选中
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CategorySection::clearSelection() {
|
||||||
|
if (!list_) return;
|
||||||
|
const QSignalBlocker block(list_); // 跨段互斥清选:被清段不回发 datasetSelected(空),避免环路/误恢复全景轴
|
||||||
|
list_->setCurrentItem(nullptr);
|
||||||
|
list_->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
void CategorySection::setChecked(const QString& dsId, bool on) {
|
void CategorySection::setChecked(const QString& dsId, bool on) {
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||||||
if ((*it)->data(0, kDsIdRole).toString() == dsId &&
|
if ((*it)->data(0, kDsIdRole).toString() == dsId &&
|
||||||
|
|
@ -264,7 +343,7 @@ void CategorySection::clearAllBusy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategorySection::refreshArrayCombo() {
|
void CategorySection::refreshArrayCombo() {
|
||||||
if (!spec_.hasArrayTypeFilter || !arrayCombo_) return;
|
if (!arrayCombo_) return; // 该段无装置筛选控件(desc_.filters 未含 ArrayType)→ 不刷
|
||||||
const QString prev = arrayCombo_->currentData().toString();
|
const QString prev = arrayCombo_->currentData().toString();
|
||||||
const QSignalBlocker block(arrayCombo_);
|
const QSignalBlocker block(arrayCombo_);
|
||||||
arrayCombo_->clear();
|
arrayCombo_->clear();
|
||||||
|
|
@ -294,8 +373,8 @@ void CategorySection::refreshArrayCombo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CategorySection::passesFilters(const DsRow& row) const {
|
bool CategorySection::passesFilters(const DsRow& row) const {
|
||||||
// 类型筛选("全部"=空不筛):按 ds 自身类型值(typeName,回退 dsTypeCode)命中选中项。
|
// 类型筛选("全部"=空不筛;无 arrayCombo_=该段不筛装置维度):按 ds 自身类型值(typeName,回退 dsTypeCode)命中选中项。
|
||||||
if (spec_.hasArrayTypeFilter && arrayCombo_) {
|
if (arrayCombo_) {
|
||||||
const QString sel = arrayCombo_->currentData().toString();
|
const QString sel = arrayCombo_->currentData().toString();
|
||||||
if (!sel.isEmpty()) {
|
if (!sel.isEmpty()) {
|
||||||
const QString t = !row.typeName.empty() ? QString::fromStdString(row.typeName)
|
const QString t = !row.typeName.empty() ? QString::fromStdString(row.typeName)
|
||||||
|
|
@ -307,7 +386,8 @@ bool CategorySection::passesFilters(const DsRow& row) const {
|
||||||
const QDate from = dateRange_ ? dateRange_->from() : QDate();
|
const QDate from = dateRange_ ? dateRange_->from() : QDate();
|
||||||
const QDate to = dateRange_ ? dateRange_->to() : QDate();
|
const QDate to = dateRange_ ? dateRange_->to() : QDate();
|
||||||
if (from.isValid() || to.isValid()) {
|
if (from.isValid() || to.isValid()) {
|
||||||
const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr;
|
// 采集时间字段定义按 ds 自身类型取(描述符无段级 dsTypeCode;逐行查更准,缺定义则回退 createTime)。
|
||||||
|
const DsTypeFields* f = dict_ ? dict_->fields(row.dsTypeCode) : nullptr;
|
||||||
std::string ts = f ? collectTimeOf(row, *f) : std::string();
|
std::string ts = f ? collectTimeOf(row, *f) : std::string();
|
||||||
if (ts.empty()) ts = row.createTime;
|
if (ts.empty()) ts = row.createTime;
|
||||||
const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd"));
|
const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd"));
|
||||||
|
|
@ -452,4 +532,86 @@ void CategorySection::showContextMenu(const QPoint& pos) {
|
||||||
menu.exec(list_->viewport()->mapToGlobal(pos));
|
menu.exec(list_->viewport()->mapToGlobal(pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CategorySection::showPlaneZPopup(QToolButton* host) {
|
||||||
|
// z 值滑块 popup(仿工具条底图滑块):拖动整体升降本类型平面(含其上全部足迹)。
|
||||||
|
// 范围 ±500 米:覆盖常见场景高程量级;滑块值即平面绝对高程(米),首开回显 lastPlaneZ_(无则 0)。
|
||||||
|
constexpr int kPlaneZRangeM = 500;
|
||||||
|
QMenu menu(this);
|
||||||
|
auto* wa = new QWidgetAction(&menu);
|
||||||
|
auto* box = new QWidget(&menu);
|
||||||
|
auto* lay = new QVBoxLayout(box);
|
||||||
|
lay->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kSm);
|
||||||
|
lay->setSpacing(space::kXs);
|
||||||
|
auto* lab = new QLabel(box);
|
||||||
|
auto* sld = new QSlider(Qt::Horizontal, box);
|
||||||
|
sld->setRange(-kPlaneZRangeM, kPlaneZRangeM);
|
||||||
|
sld->setValue(static_cast<int>(lastPlaneZ_));
|
||||||
|
sld->setMinimumWidth(160);
|
||||||
|
sld->setToolTip(QStringLiteral("平面高程 z(米)"));
|
||||||
|
auto syncLabel = [lab](int v) { lab->setText(QStringLiteral("平面 z:%1 米").arg(v)); };
|
||||||
|
syncLabel(sld->value());
|
||||||
|
// 直接平移平面/足迹/底图(改 actor position,即时同步)→ 无需防抖:valueChanged 每步直发 planeZChanged,
|
||||||
|
// 拖动即实时跟随、无移除+异步重载。lastPlaneZ_ 记住终值供重开 popup 回显。
|
||||||
|
connect(sld, &QSlider::valueChanged, this, [this, syncLabel](int v) {
|
||||||
|
lastPlaneZ_ = v;
|
||||||
|
syncLabel(v);
|
||||||
|
emit planeZChanged(QString::fromStdString(desc_.id), static_cast<double>(v));
|
||||||
|
});
|
||||||
|
lay->addWidget(lab);
|
||||||
|
lay->addWidget(sld);
|
||||||
|
wa->setDefaultWidget(box);
|
||||||
|
menu.addAction(wa);
|
||||||
|
menu.exec(host->mapToGlobal(QPoint(0, host->height())));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CategorySection::showBasemapPopup(QToolButton* host) {
|
||||||
|
// 底图 popup(仿工具条底图控件):本类型平面底图 矢量平面(默认)/无 + 透明度滑块(0–100,默认 50)。
|
||||||
|
// 类型切换为离散单次事件直发;透明度拖动逐步触发→单发 QTimer(150ms)防抖,停手后一次发射,免抖动重铺瓦片。
|
||||||
|
QMenu menu(this);
|
||||||
|
auto* wa = new QWidgetAction(&menu);
|
||||||
|
auto* box = new QWidget(&menu);
|
||||||
|
auto* lay = new QVBoxLayout(box);
|
||||||
|
lay->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kSm);
|
||||||
|
lay->setSpacing(space::kXs);
|
||||||
|
|
||||||
|
auto* kindCombo = new QComboBox(box);
|
||||||
|
kindCombo->addItem(QStringLiteral("矢量平面")); // index 0
|
||||||
|
kindCombo->addItem(QStringLiteral("无")); // index 1
|
||||||
|
kindCombo->setCurrentIndex(lastBasemapKind_);
|
||||||
|
connect(kindCombo, &QComboBox::currentIndexChanged, this, [this](int idx) {
|
||||||
|
lastBasemapKind_ = idx;
|
||||||
|
emit basemapKindChanged(QString::fromStdString(desc_.id), idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto* lab = new QLabel(box);
|
||||||
|
auto* sld = new QSlider(Qt::Horizontal, box);
|
||||||
|
sld->setRange(0, 100);
|
||||||
|
sld->setValue(lastBasemapOpacity_);
|
||||||
|
sld->setMinimumWidth(160);
|
||||||
|
sld->setToolTip(QStringLiteral("底图透明度"));
|
||||||
|
auto syncLabel = [lab](int v) { lab->setText(QStringLiteral("透明度:%1%").arg(v)); };
|
||||||
|
syncLabel(sld->value());
|
||||||
|
if (!basemapOpacityTimer_) { // 定时器 parent=this,存活于 modal popup 之外,停手后安全发射终值
|
||||||
|
basemapOpacityTimer_ = new QTimer(this);
|
||||||
|
basemapOpacityTimer_->setSingleShot(true);
|
||||||
|
basemapOpacityTimer_->setInterval(150);
|
||||||
|
connect(basemapOpacityTimer_, &QTimer::timeout, this, [this]() {
|
||||||
|
emit basemapOpacityChanged(QString::fromStdString(desc_.id), pendingBasemapOpacity_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
connect(sld, &QSlider::valueChanged, this, [this, syncLabel](int v) {
|
||||||
|
lastBasemapOpacity_ = v;
|
||||||
|
syncLabel(v);
|
||||||
|
pendingBasemapOpacity_ = v / 100.0;
|
||||||
|
basemapOpacityTimer_->start(); // 重启防抖窗口:覆盖拖动、键盘、点轨——停手后一次性发射
|
||||||
|
});
|
||||||
|
|
||||||
|
lay->addWidget(kindCombo);
|
||||||
|
lay->addWidget(lab);
|
||||||
|
lay->addWidget(sld);
|
||||||
|
wa->setDefaultWidget(box);
|
||||||
|
menu.addAction(wa);
|
||||||
|
menu.exec(host->mapToGlobal(QPoint(0, host->height())));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
|
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
|
||||||
#include "repo/CategoryConfig.hpp"
|
#include "repo/CategoryDescriptor.hpp" // geopro::data::CategoryDescriptor / OpKind / FilterKind
|
||||||
#include "repo/RepoTypes.hpp"
|
#include "repo/RepoTypes.hpp"
|
||||||
|
|
||||||
class QTreeWidget;
|
class QTreeWidget;
|
||||||
|
|
@ -22,14 +22,16 @@ class DatasetFieldDictionary;
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
class DateRangeEdit;
|
class DateRangeEdit;
|
||||||
|
class SectionIconBar;
|
||||||
|
|
||||||
// 单个数据类型大类段(spec §7):段头(标题/折叠 + 装置类型/日期筛选 + 「+新增三维体」)+ 段体(可勾选数据树)。
|
// 单个数据类型大类段(spec §7):段头(标题/折叠 + 段头图标条)+ 段体(折叠筛选行 + 可勾选数据树)。
|
||||||
// 勾选数据行 = 渲染(帘面/体素/切片);段头生成按钮据当前勾选源发 generateVolumeRequested。
|
// 段头图标条由 descriptor.operations(OpKind) 驱动;筛选行由 descriptor.filters(FilterKind) 驱动、默认折叠。
|
||||||
|
// 勾选数据行 = 渲染(帘面/体素/切片/二维面);GenerateVolume 图标据当前勾选源发 generateVolumeRequested。
|
||||||
class CategorySection : public QWidget {
|
class CategorySection : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict,
|
CategorySection(const geopro::data::CategoryDescriptor& desc,
|
||||||
QWidget* parent = nullptr);
|
geopro::data::DatasetFieldDictionary* dict, QWidget* parent = nullptr);
|
||||||
|
|
||||||
// 对象树同源的扁平 GS/TM 节点(段体容器分层用;Task 12 接入真实结构,当前仅存储)。
|
// 对象树同源的扁平 GS/TM 节点(段体容器分层用;Task 12 接入真实结构,当前仅存储)。
|
||||||
void setStructure(const std::vector<geopro::data::StructNode>& nodes);
|
void setStructure(const std::vector<geopro::data::StructNode>& nodes);
|
||||||
|
|
@ -39,20 +41,34 @@ public:
|
||||||
void setBusy(const QString& dsId, bool busy);
|
void setBusy(const QString& dsId, bool busy);
|
||||||
void clearAllBusy(); // 撤回本段所有 spinner(失败兜底)
|
void clearAllBusy(); // 撤回本段所有 spinner(失败兜底)
|
||||||
void selectItem(const QString& dsId); // 程序化选中某行(VTK→list 反向联动);空/未找到=清选中
|
void selectItem(const QString& dsId); // 程序化选中某行(VTK→list 反向联动);空/未找到=清选中
|
||||||
|
void clearSelection(); // 清本段树选中(信号阻断,不回发 datasetSelected);供跨段全列互斥用
|
||||||
QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds(异常显隐同步用)
|
QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds(异常显隐同步用)
|
||||||
void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉
|
void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉
|
||||||
const CategorySpec& spec() const { return spec_; }
|
|
||||||
bool isExpanded() const; // 段头展开态(供外层按折叠状态重分配 stretch,实现"折叠向上收")
|
bool isExpanded() const; // 段头展开态(供外层按折叠状态重分配 stretch,实现"折叠向上收")
|
||||||
void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见
|
void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见
|
||||||
QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用)
|
QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用)
|
||||||
QTreeWidgetItem* itemFor(const QString& dsId) const; // 按 dsId 取行(无则 nullptr)
|
QTreeWidgetItem* itemFor(const QString& dsId) const; // 按 dsId 取行(无则 nullptr)
|
||||||
|
// 该 ds 所在层级子树的全部 dsId(贴合坐标轴子树盒,spec §2/§3.2 决策 3):先归一到子树根
|
||||||
|
// (向上找最高非容器祖先=三维体行),再自根向下收集整棵子树(体+切片+异常)的非空 dsId。
|
||||||
|
// 故选中体/切片/异常都归到同一个「该体子树」盒。dsId 不在本段则返回空。
|
||||||
|
QStringList subtreeDsIds(const QString& dsId) const;
|
||||||
|
bool hasRenderableRows() const; // 段体是否含可渲染数据行(非 container 容器节点),供单列动态显隐
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// 段体树 viewport 事件过滤:在默认处理改选之前记录「按下瞬间该行是否已选中」,供单击已选行的 toggle 取消判定。
|
||||||
|
bool eventFilter(QObject* obj, QEvent* ev) override;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染
|
void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染
|
||||||
void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch
|
void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch
|
||||||
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」
|
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」(接收方按 sourceIds 解析类型)
|
||||||
void radarImportRequested(bool impulse); // 三维体段头「+导入雷达测线」(false=规范化 .head/.data, true=Impulse .iprb)
|
void planeZChanged(const QString& typeId, double z); // PlaneZ 滑块:整体升降该 2D 类型平面(z=绝对高程,米)
|
||||||
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情
|
void basemapKindChanged(const QString& typeId, int kind); // 底图弹窗:矢量平面(0)/无(1)
|
||||||
|
void basemapOpacityChanged(const QString& typeId, double o); // 底图弹窗透明度滑块[0,1](防抖发射)
|
||||||
|
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 右键「详情」=属性弹窗
|
||||||
|
// 双击某行(决策6/T4):适配相机到该 ds 子树空间范围 + 联动中下方详情面板(无详情页类型静默)。
|
||||||
|
// 与 detailRequested(右键属性弹窗)分开,使双击「只适配 + 图表联动」,三维体等无图表页时静默。
|
||||||
|
void datasetActivated(const QString& dsId, const QString& ddCode, const QString& name);
|
||||||
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常)
|
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常)
|
||||||
// ── 三维体段右键操作(迁自旧 Column3DAnalysis,全接)──
|
// ── 三维体段右键操作(迁自旧 Column3DAnalysis,全接)──
|
||||||
void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); // 体→生成切片(轴+目标体)
|
void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); // 体→生成切片(轴+目标体)
|
||||||
|
|
@ -67,24 +83,37 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void showContextMenu(const QPoint& pos); // 段体树右键菜单(详情 + 删除)
|
void showContextMenu(const QPoint& pos); // 段体树右键菜单(详情 + 删除)
|
||||||
|
void showPlaneZPopup(QToolButton* host); // PlaneZ 图标:弹 z 值滑块 popup → planeZChanged
|
||||||
|
void showBasemapPopup(QToolButton* host); // Basemap 图标:弹 矢量平面/无 + 透明度滑块 popup
|
||||||
void rebuildList(); // 据 rows_(经装置/日期筛选)重建段体树并复原勾选
|
void rebuildList(); // 据 rows_(经装置/日期筛选)重建段体树并复原勾选
|
||||||
void refreshArrayCombo(); // 据当前 rows_ 重填装置类型下拉项(经字典 value→中文)
|
void refreshArrayCombo(); // 据当前 rows_ 重填装置类型下拉项(经字典 value→中文)
|
||||||
void emitChecked(); // 收集勾选 → checkedDatasetsChanged
|
void emitChecked(); // 收集勾选 → checkedDatasetsChanged
|
||||||
QStringList checkedDsIds() const;
|
QStringList checkedDsIds() const;
|
||||||
bool passesFilters(const geopro::data::DsRow& row) const; // 装置类型 + 采集时间范围
|
bool passesFilters(const geopro::data::DsRow& row) const; // 装置类型 + 采集时间范围
|
||||||
|
|
||||||
CategorySpec spec_;
|
geopro::data::CategoryDescriptor desc_;
|
||||||
geopro::data::DatasetFieldDictionary* dict_ = nullptr;
|
geopro::data::DatasetFieldDictionary* dict_ = nullptr;
|
||||||
std::vector<geopro::data::DsRow> rows_;
|
std::vector<geopro::data::DsRow> rows_;
|
||||||
std::vector<geopro::data::StructNode> structure_;
|
std::vector<geopro::data::StructNode> structure_;
|
||||||
|
|
||||||
QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头)
|
QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头)
|
||||||
|
SectionIconBar* iconBar_ = nullptr; // 段头响应式图标条(由 desc_.operations 装配)
|
||||||
QWidget* body_ = nullptr; // 段体容器(折叠时隐藏)
|
QWidget* body_ = nullptr; // 段体容器(折叠时隐藏)
|
||||||
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 hasArrayTypeFilter)
|
QWidget* filterRow_ = nullptr; // 筛选行容器(默认折叠,由 Filter 图标 toggle)
|
||||||
|
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 desc_.filters 含 ArrayType)
|
||||||
DateRangeEdit* dateRange_ = nullptr; // 采集时间范围筛选(起止双日历,可清空)
|
DateRangeEdit* dateRange_ = nullptr; // 采集时间范围筛选(起止双日历,可清空)
|
||||||
QTreeWidget* list_ = nullptr;
|
QTreeWidget* list_ = nullptr;
|
||||||
QTimer* spinTimer_ = nullptr; // 驱动 busy 行 spinner 旋转(有 busy 行时运行)
|
QTimer* spinTimer_ = nullptr; // 驱动 busy 行 spinner 旋转(有 busy 行时运行)
|
||||||
int spinAngle_ = 0; // 当前 spinner 角度(度)
|
int spinAngle_ = 0; // 当前 spinner 角度(度)
|
||||||
|
double lastPlaneZ_ = 0.0; // 上次 z 值滑块设定的平面高程(重开 popup 时回显,无则 0;直接平移故无防抖)
|
||||||
|
int lastBasemapKind_ = 0; // 上次底图选择(0=矢量平面/1=无),重开 popup 时回显
|
||||||
|
int lastBasemapOpacity_ = 50; // 上次底图透明度(0–100,重开 popup 回显;默认 50)
|
||||||
|
QTimer* basemapOpacityTimer_ = nullptr; // 底图透明度滑块发射防抖(透明度改动会触发瓦片重铺,仍需防抖)
|
||||||
|
double pendingBasemapOpacity_ = 0.5; // 防抖待发的底图透明度[0,1](定时器到点发射 basemapOpacityChanged)
|
||||||
|
// 单击已选行取消选中(toggle-off):eventFilter 于按下瞬间(默认改选前)记录被按行及其选中态;itemClicked 据此取消。
|
||||||
|
QTreeWidgetItem* pressedItem_ = nullptr; // 本次左键按下的行(itemAt(press),与 itemClicked 的行比对)
|
||||||
|
bool pressedSelected_ = false; // 按下瞬间该行是否已选中(true 且非勾选框点击时,单击取消选中)
|
||||||
|
bool checkToggledThisPress_ = false; // 本次按下是否切换了勾选框(点勾选框只改渲染,不取消选中)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
#include "panels/columns/Column2DDataset.hpp"
|
|
||||||
|
|
||||||
#include <set>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
|
|
||||||
#include "EmptyAwareComboBox.hpp"
|
|
||||||
#include <QDoubleSpinBox>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QSignalBlocker>
|
|
||||||
#include <QTreeWidget>
|
|
||||||
#include <QTreeWidgetItemIterator>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "Theme.hpp"
|
|
||||||
#include "panels/DatasetListPanel.hpp"
|
|
||||||
|
|
||||||
namespace geopro::app {
|
|
||||||
|
|
||||||
Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) {
|
|
||||||
auto* root = new QVBoxLayout(this);
|
|
||||||
root->setContentsMargins(space::kMd, space::kMd, space::kMd, space::kMd);
|
|
||||||
root->setSpacing(space::kMd);
|
|
||||||
|
|
||||||
// 地图
|
|
||||||
{
|
|
||||||
auto* form = new QFormLayout();
|
|
||||||
auto* basemap = new EmptyAwareComboBox();
|
|
||||||
basemap->addItem(QStringLiteral("天地图"));
|
|
||||||
basemap->addItem(QStringLiteral("Google Map"));
|
|
||||||
basemap->addItem(QStringLiteral("隐藏"));
|
|
||||||
basemap->setCurrentIndex(0); // 默认天地图:数据重锚后由 onFrameReanchored 在数据位置加载
|
|
||||||
connect(basemap, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
|
||||||
[this](int index) { emit basemapChanged(index); });
|
|
||||||
form->addRow(QStringLiteral("底图源"), basemap);
|
|
||||||
root->addWidget(new QLabel(QStringLiteral("地图")));
|
|
||||||
root->addLayout(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2D视图
|
|
||||||
{
|
|
||||||
auto* form = new QFormLayout();
|
|
||||||
auto* view2d = new EmptyAwareComboBox();
|
|
||||||
view2d->addItem(QStringLiteral("关闭"));
|
|
||||||
view2d->addItem(QStringLiteral("Z=0"));
|
|
||||||
view2d->addItem(QStringLiteral("顶部"));
|
|
||||||
view2d->addItem(QStringLiteral("底部"));
|
|
||||||
view2d->addItem(QStringLiteral("自定义"));
|
|
||||||
view2d->setCurrentIndex(1);
|
|
||||||
auto* zSpin = new QDoubleSpinBox();
|
|
||||||
zSpin->setRange(-1000000, 1000000);
|
|
||||||
zSpin->setSuffix(QStringLiteral(" m"));
|
|
||||||
zSpin->setValue(0);
|
|
||||||
connect(view2d, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
|
||||||
[this, form, zSpin](int idx) {
|
|
||||||
form->setRowVisible(zSpin, idx == 4); // 整行隐藏(含"Z 值"标签),非自定义时不留孤标签
|
|
||||||
emit view2DModeChanged(idx);
|
|
||||||
});
|
|
||||||
connect(zSpin, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
|
|
||||||
[this](double z) { emit customZChanged(z); });
|
|
||||||
form->addRow(QStringLiteral("位置"), view2d);
|
|
||||||
form->addRow(QStringLiteral("Z 值"), zSpin);
|
|
||||||
form->setRowVisible(zSpin, false); // 默认非自定义→隐藏整行
|
|
||||||
root->addWidget(new QLabel(QStringLiteral("2D视图")));
|
|
||||||
root->addLayout(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据集列表(可勾选)
|
|
||||||
list_ = new QTreeWidget();
|
|
||||||
list_->setHeaderHidden(true);
|
|
||||||
list_->setRootIsDecorated(true);
|
|
||||||
list_->setSelectionMode(QAbstractItemView::ExtendedSelection); // 多选行(与 VTK 多选拖动联动)
|
|
||||||
applyDatasetCardDelegate(list_);
|
|
||||||
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
|
||||||
QStringList ids;
|
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
|
||||||
if ((*it)->checkState(0) == Qt::Checked)
|
|
||||||
ids << (*it)->data(0, kDsIdRole).toString();
|
|
||||||
}
|
|
||||||
emit checkedDatasetsChanged(ids);
|
|
||||||
});
|
|
||||||
// 行选中变化 → 上抛选中 dsId(高亮联动 VTK;与勾选/渲染独立)。
|
|
||||||
connect(list_, &QTreeWidget::itemSelectionChanged, this, [this]() {
|
|
||||||
QStringList ids;
|
|
||||||
for (QTreeWidgetItem* it : list_->selectedItems())
|
|
||||||
ids << it->data(0, kDsIdRole).toString();
|
|
||||||
emit selectedDatasetsChanged(ids);
|
|
||||||
});
|
|
||||||
root->addWidget(list_, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Column2DDataset::setDatasets(const std::vector<geopro::data::DsRow>& rows) {
|
|
||||||
// 增量保留:记住当前已勾选的足迹 ds,重建后复原(仍存在的项保持勾选)。否则对象树每次增删勾选都触发
|
|
||||||
// 本刷新 → 清空全部勾选 + 上抛空集 → 已渲染足迹被移除、列表选中丢失(用户反馈:必须增量更新,
|
|
||||||
// 与三维分析段 CategorySection::rebuildList 同一处理)。
|
|
||||||
std::set<std::string> wasChecked;
|
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
|
||||||
if ((*it)->checkState(0) == Qt::Checked)
|
|
||||||
wasChecked.insert((*it)->data(0, kDsIdRole).toString().toStdString());
|
|
||||||
|
|
||||||
{
|
|
||||||
QSignalBlocker blocker(list_);
|
|
||||||
populateDatasetList(list_, rows, /*append=*/false);
|
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
|
||||||
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
|
|
||||||
const std::string id = (*it)->data(0, kDsIdRole).toString().toStdString();
|
|
||||||
// 复原勾选:仍存在的曾勾选项保持勾选;新项默认不勾。
|
|
||||||
(*it)->setCheckState(0, wasChecked.count(id) ? Qt::Checked : Qt::Unchecked);
|
|
||||||
}
|
|
||||||
} // blocker released here
|
|
||||||
// 上抛复原后的勾选集(保持渲染,不再清空 → 控制器据 diff 增量保留已渲染足迹,集合不变则不增删)。
|
|
||||||
QStringList ids;
|
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
|
||||||
if ((*it)->checkState(0) == Qt::Checked)
|
|
||||||
ids << (*it)->data(0, kDsIdRole).toString();
|
|
||||||
emit checkedDatasetsChanged(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Column2DDataset::setSelectedDsIds(const QStringList& dsIds) {
|
|
||||||
QSignalBlocker blocker(list_); // 防回环:VTK→列表 设置选中不再上抛 selectedDatasetsChanged
|
|
||||||
list_->clearSelection();
|
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
|
||||||
if (dsIds.contains((*it)->data(0, kDsIdRole).toString())) (*it)->setSelected(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace geopro::app
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <vector>
|
|
||||||
#include "repo/RepoTypes.hpp"
|
|
||||||
|
|
||||||
class QTreeWidget;
|
|
||||||
|
|
||||||
namespace geopro::app {
|
|
||||||
|
|
||||||
// 二维数据集栏:地图 + 2D视图(含自定义 Z) + 2D 数据集列表。
|
|
||||||
class Column2DDataset : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit Column2DDataset(QWidget* parent = nullptr);
|
|
||||||
void setDatasets(const std::vector<geopro::data::DsRow>& rows);
|
|
||||||
// VTK→列表 选择联动:按 dsId 选中对应行(高亮),内部屏蔽信号避免回环。
|
|
||||||
void setSelectedDsIds(const QStringList& dsIds);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void basemapChanged(int index); // 0 天地图 / 1 Google / 2 隐藏
|
|
||||||
void view2DModeChanged(int index); // 0 关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义
|
|
||||||
void customZChanged(double z); // 世界绝对高程(米),向上为正
|
|
||||||
void checkedDatasetsChanged(const QStringList& dsIds); // 勾选(渲染开关)变化
|
|
||||||
void selectedDatasetsChanged(const QStringList& dsIds); // 行选中(高亮联动)变化,非勾选
|
|
||||||
|
|
||||||
private:
|
|
||||||
QTreeWidget* list_ = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace geopro::app
|
|
||||||
|
|
@ -1,34 +1,19 @@
|
||||||
#include "panels/columns/ColumnDrawer.hpp"
|
#include "panels/columns/ColumnDrawer.hpp"
|
||||||
#include "panels/columns/Column2DDataset.hpp"
|
|
||||||
#include "panels/columns/CategoryAnalysisTab.hpp"
|
#include "panels/columns/CategoryAnalysisTab.hpp"
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QTabBar>
|
|
||||||
#include <QTabWidget>
|
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary* dict)
|
ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary* dict)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
col2D_ = new Column2DDataset(this);
|
// 单列承载:去 QTabWidget,body_ 直接为 CategoryAnalysisTab(含 trajectory 段,二维并入同列)。
|
||||||
analysisTab_ = new CategoryAnalysisTab(dict, this);
|
analysisTab_ = new CategoryAnalysisTab(dict, this);
|
||||||
|
body_ = analysisTab_;
|
||||||
// Tab 容器(body_):两 tab(三维分析[分段] / 二维分析)。
|
|
||||||
auto* tabs = new QTabWidget(this);
|
|
||||||
body_ = tabs;
|
|
||||||
tabs->addTab(analysisTab_, QStringLiteral("三维分析"));
|
|
||||||
tabs->addTab(col2D_, QStringLiteral("二维分析"));
|
|
||||||
tabs->tabBar()->setUsesScrollButtons(false); // 永不出左右滚动箭头(两 tab 必能平铺)
|
|
||||||
// 切 tab → 发 analysisModeChanged(is2D):以"当前 widget 是否 col2D"判定,不写死索引。
|
|
||||||
connect(tabs, &QTabWidget::currentChanged, this, [this, tabs](int idx) {
|
|
||||||
emit analysisModeChanged(tabs->widget(idx) == col2D_);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 折叠按钮:固定宽 18px,垂直拉伸。
|
// 折叠按钮:固定宽 18px,垂直拉伸。
|
||||||
// 用 SVG 图标(makeGlyph)而非 ◀/▶ 文字——三角符(U+25C0/25B6)不在 YaHei,作按钮文字会触发
|
// 用 SVG 图标(makeGlyph)而非 ◀/▶ 文字——三角符(U+25C0/25B6)不在 YaHei,作按钮文字会触发
|
||||||
|
|
@ -58,22 +43,6 @@ ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary
|
||||||
setMaximumWidth(560);
|
setMaximumWidth(560);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColumnDrawer::resizeEvent(QResizeEvent* e)
|
|
||||||
{
|
|
||||||
QWidget::resizeEvent(e);
|
|
||||||
// 两 tab 平分抽屉宽度填满(带样式表的 tab 不响应 setExpanding,须按 barWidth/n 显式给宽)。
|
|
||||||
// 消除旧 3 栏布局遗留的右侧空白——重构成 2 栏后不再三分、留空第三位。
|
|
||||||
if (auto* tabs = qobject_cast<QTabWidget*>(body_)) {
|
|
||||||
const int n = tabs->count();
|
|
||||||
if (n > 0 && tabs->width() > 0) {
|
|
||||||
// 每 tab 内容宽 = 总宽/n - 每 tab 非内容开销(全局 QSS padding 8+16+16=… 约 32 + margin 4)。
|
|
||||||
// 稍欠一点宽避免溢出(溢出会触发滚动箭头);setUsesScrollButtons(false) 再兜底。
|
|
||||||
const int w = std::max(40, tabs->width() / n - 42);
|
|
||||||
tabs->tabBar()->setStyleSheet(QStringLiteral("QTabBar::tab{width:%1px;}").arg(w));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ColumnDrawer::toggleCollapsed()
|
void ColumnDrawer::toggleCollapsed()
|
||||||
{
|
{
|
||||||
collapsed_ = !collapsed_;
|
collapsed_ = !collapsed_;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QResizeEvent;
|
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
class DatasetFieldDictionary;
|
class DatasetFieldDictionary;
|
||||||
|
|
@ -10,36 +9,29 @@ class DatasetFieldDictionary;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
class Column2DDataset;
|
|
||||||
class CategoryAnalysisTab;
|
class CategoryAnalysisTab;
|
||||||
|
|
||||||
// VTK视图左侧内嵌抽屉:两 tab(三维分析[按数据类型分段]/二维分析) + 折叠开关。
|
// VTK视图左侧内嵌抽屉:单列承载 CategoryAnalysisTab(按数据类型分段,含 trajectory 段) + 折叠开关。
|
||||||
|
// B2 去 tab:原「三维分析 / 二维分析」双 tab 合一,二维(足迹/轨迹)经描述符分段并入同一列。
|
||||||
class ColumnDrawer : public QWidget {
|
class ColumnDrawer : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ColumnDrawer(QWidget* parent = nullptr,
|
explicit ColumnDrawer(QWidget* parent = nullptr,
|
||||||
geopro::data::DatasetFieldDictionary* dict = nullptr);
|
geopro::data::DatasetFieldDictionary* dict = nullptr);
|
||||||
|
|
||||||
Column2DDataset* col2D() const { return col2D_; }
|
|
||||||
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
|
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
// 切换「三维分析 / 二维分析」tab:is2D=true 进入二维分析。上层据此切相机+翻维度可见标志。
|
|
||||||
void analysisModeChanged(bool is2D);
|
|
||||||
// 折叠/展开切换:上层据此调整 QSplitter 尺寸,回收/恢复抽屉栏宽(否则收起残留空白区)。
|
// 折叠/展开切换:上层据此调整 QSplitter 尺寸,回收/恢复抽屉栏宽(否则收起残留空白区)。
|
||||||
void collapsedChanged(bool collapsed);
|
void collapsedChanged(bool collapsed);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void toggleCollapsed();
|
void toggleCollapsed();
|
||||||
void expand(); // 强制展开(进入全屏时确保三栏可见)
|
void expand(); // 强制展开(进入全屏时确保单列可见)
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* e) override; // 两 tab 按抽屉宽平分(消除右侧空白"第三栏位")
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Column2DDataset* col2D_ = nullptr;
|
|
||||||
CategoryAnalysisTab* analysisTab_ = nullptr;
|
CategoryAnalysisTab* analysisTab_ = nullptr;
|
||||||
QWidget* body_ = nullptr; // QTabWidget,折叠时隐藏
|
QWidget* body_ = nullptr; // = analysisTab_(折叠时隐藏)
|
||||||
QPushButton* toggleBtn_ = nullptr;
|
QPushButton* toggleBtn_ = nullptr;
|
||||||
bool collapsed_ = false;
|
bool collapsed_ = false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons) {
|
||||||
|
if (totalIcons <= 0 || iconPx <= 0) return 0;
|
||||||
|
const int ideal = std::min(totalIcons, std::max(0, maxIcons));
|
||||||
|
const bool overflowFromCap = totalIcons > ideal; // 超 max → 必有溢出位
|
||||||
|
// 先看理想数能否放下(若已因 cap 溢出,理想数也要含溢出位)
|
||||||
|
auto fits = [&](int n, bool withOverflow) {
|
||||||
|
return n * iconPx + (withOverflow ? overflowPx : 0) <= availablePx;
|
||||||
|
};
|
||||||
|
int n = ideal;
|
||||||
|
bool overflow = overflowFromCap;
|
||||||
|
while (n > 0 && !fits(n, overflow || (totalIcons > n))) {
|
||||||
|
--n;
|
||||||
|
overflow = true; // 一旦减少必有「…」
|
||||||
|
}
|
||||||
|
if (n < 0) n = 0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int kGlyphPx = 18; // glyph 绘制像素;按钮本体宽用 iconPx_,与溢出计算口径一致
|
||||||
|
|
||||||
|
// glyphKey 字符串 → Glyph 枚举(C2 按段头操作填键;未知键回退中性图标)。
|
||||||
|
Glyph glyphFromKey(const QString& key) {
|
||||||
|
const QString k = key.toLower();
|
||||||
|
if (k == QStringLiteral("plus")) return Glyph::Plus;
|
||||||
|
if (k == QStringLiteral("filter")) return Glyph::Filter;
|
||||||
|
if (k == QStringLiteral("upload")) return Glyph::Upload;
|
||||||
|
if (k == QStringLiteral("download")) return Glyph::Download;
|
||||||
|
if (k == QStringLiteral("collapse")) return Glyph::Collapse;
|
||||||
|
if (k == QStringLiteral("fullscreen")) return Glyph::Fullscreen;
|
||||||
|
if (k == QStringLiteral("map")) return Glyph::Map;
|
||||||
|
if (k == QStringLiteral("detail")) return Glyph::Detail;
|
||||||
|
if (k == QStringLiteral("property")) return Glyph::Property;
|
||||||
|
if (k == QStringLiteral("gear")) return Glyph::Gear;
|
||||||
|
return Glyph::Property;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SectionIconBar::SectionIconBar(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QHBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
// 水平 Preferred:优先取 sizeHint(全图标宽),列变窄受压时可缩向 minimumSizeHint(仅「…」);
|
||||||
|
// 垂直 Fixed:高度恒为图标按钮高。不可用 Fixed-Fixed(永不折叠)或 Ignored(丢 sizeHint)。
|
||||||
|
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize SectionIconBar::sizeHint() const {
|
||||||
|
// 默认上限内的图标全显所需宽度:min(操作数, maxIcons_) 个按钮,各占 iconPx_。
|
||||||
|
const int count = std::min(static_cast<int>(actions_.size()), std::max(0, maxIcons_));
|
||||||
|
return QSize(count * iconPx_, iconPx_);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize SectionIconBar::minimumSizeHint() const {
|
||||||
|
// 最窄也要放下「…」溢出按钮,使本条可缩到仅剩「…」。
|
||||||
|
return QSize(overflowPx_, iconPx_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SectionIconBar::setActions(const std::vector<IconAction>& a) {
|
||||||
|
// 清旧按钮与溢出按钮
|
||||||
|
for (auto* b : btns_) {
|
||||||
|
if (b) b->deleteLater();
|
||||||
|
}
|
||||||
|
btns_.clear();
|
||||||
|
if (overflowBtn_) {
|
||||||
|
overflowBtn_->deleteLater();
|
||||||
|
overflowBtn_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions_ = a;
|
||||||
|
auto* lay = qobject_cast<QHBoxLayout*>(layout());
|
||||||
|
const QColor ic = tokenColor("text/secondary");
|
||||||
|
|
||||||
|
for (const IconAction& act : actions_) {
|
||||||
|
auto* b = new QToolButton(this);
|
||||||
|
b->setIcon(makeGlyph(glyphFromKey(act.glyphKey), ic, kGlyphPx));
|
||||||
|
b->setIconSize(QSize(kGlyphPx, kGlyphPx));
|
||||||
|
b->setAutoRaise(true);
|
||||||
|
b->setToolTip(act.tooltip);
|
||||||
|
b->setFixedSize(iconPx_, iconPx_);
|
||||||
|
if (act.popupBuilder) {
|
||||||
|
const auto builder = act.popupBuilder;
|
||||||
|
QToolButton* host = b;
|
||||||
|
connect(b, &QToolButton::clicked, this, [builder, host] { builder(host); });
|
||||||
|
} else if (act.onClick) {
|
||||||
|
const auto cb = act.onClick;
|
||||||
|
connect(b, &QToolButton::clicked, this, [cb] { cb(); });
|
||||||
|
}
|
||||||
|
if (lay) lay->addWidget(b);
|
||||||
|
btns_.push_back(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 末尾「…」溢出按钮:即点即弹菜单
|
||||||
|
overflowBtn_ = new QToolButton(this);
|
||||||
|
overflowBtn_->setText(QStringLiteral("…"));
|
||||||
|
overflowBtn_->setAutoRaise(true);
|
||||||
|
overflowBtn_->setToolTip(QStringLiteral("更多"));
|
||||||
|
overflowBtn_->setFixedSize(overflowPx_, iconPx_);
|
||||||
|
overflowBtn_->setPopupMode(QToolButton::InstantPopup);
|
||||||
|
overflowBtn_->setMenu(new QMenu(overflowBtn_));
|
||||||
|
if (lay) lay->addWidget(overflowBtn_);
|
||||||
|
|
||||||
|
updateGeometry(); // 操作数变化 → 通知父布局重新按新 sizeHint 分配宽度
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SectionIconBar::resizeEvent(QResizeEvent* e) {
|
||||||
|
QWidget::resizeEvent(e);
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SectionIconBar::relayout() {
|
||||||
|
const int total = static_cast<int>(actions_.size());
|
||||||
|
const int vis = visibleIconCount(total, width(), iconPx_, overflowPx_, maxIcons_);
|
||||||
|
|
||||||
|
QMenu* menu = overflowBtn_ ? overflowBtn_->menu() : nullptr;
|
||||||
|
if (menu) menu->clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < total; ++i) {
|
||||||
|
QToolButton* b = btns_[static_cast<size_t>(i)];
|
||||||
|
if (!b) continue;
|
||||||
|
if (i < vis) {
|
||||||
|
b->setVisible(true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
b->setVisible(false);
|
||||||
|
if (!menu) continue;
|
||||||
|
const IconAction& act = actions_[static_cast<size_t>(i)];
|
||||||
|
QAction* ma = menu->addAction(act.tooltip);
|
||||||
|
if (act.popupBuilder) {
|
||||||
|
const auto builder = act.popupBuilder;
|
||||||
|
QToolButton* host = overflowBtn_;
|
||||||
|
connect(ma, &QAction::triggered, this, [builder, host] { builder(host); });
|
||||||
|
} else if (act.onClick) {
|
||||||
|
const auto cb = act.onClick;
|
||||||
|
connect(ma, &QAction::triggered, this, [cb] { cb(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overflowBtn_) overflowBtn_->setVisible(vis < total);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class QToolButton;
|
||||||
|
class QResizeEvent;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 纯逻辑:给定约束返回可见图标数(其余收进「…」)。见 spec §6。
|
||||||
|
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons);
|
||||||
|
|
||||||
|
// 段头响应式图标工具条:≤maxIcons 且宽度够则全显;否则右侧依次收进末尾「…」下拉。
|
||||||
|
struct IconAction {
|
||||||
|
QString glyphKey; // 图标键(映射到 Glyph)
|
||||||
|
QString tooltip;
|
||||||
|
std::function<void()> onClick; // 直接动作;为空则用 popupBuilder
|
||||||
|
std::function<void(QToolButton*)> popupBuilder; // 弹 popup(z值/底图/筛选用)
|
||||||
|
};
|
||||||
|
|
||||||
|
class SectionIconBar : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SectionIconBar(QWidget* parent = nullptr);
|
||||||
|
void setActions(const std::vector<IconAction>& actions); // 重建按钮
|
||||||
|
void setMaxIcons(int n) { maxIcons_ = n; updateGeometry(); relayout(); }
|
||||||
|
// sizeHint:声明放下「默认上限内全部图标」所需宽度,使段头 HBox 分给本条真实宽度
|
||||||
|
// (否则 relayout 在 width()=0 时折叠全部图标→内层布局尺寸塌缩成只剩「…」→恒折叠,见 spec §6)。
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
// minimumSizeHint:至少容下「…」溢出按钮,列足够窄时本条可缩到仅剩「…」。
|
||||||
|
QSize minimumSizeHint() const override;
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent* e) override;
|
||||||
|
private:
|
||||||
|
void relayout(); // 按当前宽度算可见数,多余进「…」菜单
|
||||||
|
std::vector<IconAction> actions_;
|
||||||
|
std::vector<QToolButton*> btns_;
|
||||||
|
QToolButton* overflowBtn_ = nullptr;
|
||||||
|
int maxIcons_ = 3;
|
||||||
|
int iconPx_ = 30;
|
||||||
|
int overflowPx_ = 30;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -3,6 +3,7 @@ add_library(geopro_controller STATIC
|
||||||
WorkbenchNavController.cpp
|
WorkbenchNavController.cpp
|
||||||
DatasetDetailController.cpp
|
DatasetDetailController.cpp
|
||||||
DatasetViewState.cpp
|
DatasetViewState.cpp
|
||||||
|
DatasetRenderStrategy.cpp
|
||||||
VtkSceneController.cpp)
|
VtkSceneController.cpp)
|
||||||
target_include_directories(geopro_controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_controller PUBLIC geopro_data Qt6::Core)
|
target_link_libraries(geopro_controller PUBLIC geopro_data Qt6::Core)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ public slots:
|
||||||
void loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo,
|
void loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo,
|
||||||
int pageSize);
|
int pageSize);
|
||||||
void focusDataset(const QString& dsId);
|
void focusDataset(const QString& dsId);
|
||||||
|
public:
|
||||||
|
// 该 ddCode 是否有已注册的详情页策略(=有中下方图表详情页)。供联动入口 gate:
|
||||||
|
// 无策略的类型(如 dd_voxel/dd_radar_3d 三维体)不走 openDataset,避免 loadFailed 的状态栏提示,静默。
|
||||||
|
bool supports(const QString& ddCode) const { return registry_.supports(ddCode.toStdString()); }
|
||||||
signals:
|
signals:
|
||||||
void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
const QString& tmObjectId, const std::vector<controller::TabSpec>& tabs);
|
const QString& tmObjectId, const std::vector<controller::TabSpec>& tabs);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
#include "controller/DatasetRenderStrategy.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "controller/VtkSceneController.hpp" // 完整类型 + 含 I3dSceneView(view_.removeDataset)
|
||||||
|
|
||||||
|
namespace geopro::controller {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr double kDefaultBasemapOpacity = 0.5; // 平面底图默认半透明(地下足迹可透过看)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 各策略委托回控制器既有渲染路径(友元访问其私有 addDatasetAsync/add2DDatasetAsync/view_)。
|
||||||
|
// 增量 gen 沿用控制器当前 rebuildGeneration_(不自增,与并发增量互不作废)。
|
||||||
|
void VolumeRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
|
||||||
|
ctrl_.addDatasetAsync(dsId, ctrl_.rebuildGeneration_);
|
||||||
|
}
|
||||||
|
void VolumeRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
|
||||||
|
|
||||||
|
void CurtainRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
|
||||||
|
ctrl_.addDatasetAsync(dsId, ctrl_.rebuildGeneration_);
|
||||||
|
}
|
||||||
|
void CurtainRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
|
||||||
|
|
||||||
|
void Plane2DRenderStrategy::add(const std::string& typeId, const std::string& dsId) {
|
||||||
|
// 该类型平面 z:首勾以 dsZ(场景地表基准 zRefElev)定平面,后续勾选返回既有平面 z(投影)。
|
||||||
|
const double dsZ = ctrl_.view_.zRefElev();
|
||||||
|
const double z = planeReg_.onChecked(typeId, dsId, dsZ);
|
||||||
|
dsToType_[dsId] = typeId; // remove 仅得 dsId,记下归属类型以回收平面成员
|
||||||
|
ctrl_.add2DDatasetAsync(dsId, ctrl_.rebuildGeneration_, z);
|
||||||
|
}
|
||||||
|
void Plane2DRenderStrategy::remove(const std::string& dsId) {
|
||||||
|
ctrl_.view_.removeDataset(dsId);
|
||||||
|
auto it = dsToType_.find(dsId);
|
||||||
|
if (it == dsToType_.end()) return;
|
||||||
|
planeReg_.onUnchecked(it->second, dsId); // 该类型成员集空时平面自动消失
|
||||||
|
dsToType_.erase(it);
|
||||||
|
}
|
||||||
|
// 该类型首勾(控制器活跃计数 0→1,先于 add 触发):建该类型平面矢量底图。
|
||||||
|
// 注意 onTypeActivated 在 add 之前触发 → 此刻 planeReg_ 尚无该平面,createBasemap 以 zRefElev() 兜底
|
||||||
|
// (即首勾 add 即将写入的平面 z,二者一致)。kind/opacity 复位为默认(矢量平面/0.5)。
|
||||||
|
void Plane2DRenderStrategy::onTypeActivated(const std::string& typeId) {
|
||||||
|
bmKind_[typeId] = 0;
|
||||||
|
bmOpacity_[typeId] = kDefaultBasemapOpacity;
|
||||||
|
createBasemap(typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 该类型全消(控制器活跃计数 1→0):销毁该类型底图(析构移除瓦片→底图随平面一并消失),遗忘其 kind/opacity。
|
||||||
|
void Plane2DRenderStrategy::onTypeDeactivated(const std::string& typeId) {
|
||||||
|
bms_.erase(typeId);
|
||||||
|
bmKind_.erase(typeId);
|
||||||
|
bmOpacity_.erase(typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plane2DRenderStrategy::setPlaneZ(const std::string& typeId, double z) {
|
||||||
|
planeReg_.setPlaneZ(typeId, z); // 平面 z 真源更新(类型不存在则无操作)
|
||||||
|
// 直接平移该类型全部已勾选足迹:只改足迹 actor 的 position(无移除+异步重载)→ 拖滑块即时跟随、无闪烁。
|
||||||
|
std::vector<std::string> dsIds;
|
||||||
|
for (const auto& [dsId, t] : dsToType_)
|
||||||
|
if (t == typeId) dsIds.push_back(dsId);
|
||||||
|
if (!dsIds.empty()) ctrl_.view_.setMapLinesZ(dsIds, z);
|
||||||
|
// 底图同步平移:直接改瓦片 position(无销毁+重建、无重下载)→ 底图与足迹一同实时跟随滑块。
|
||||||
|
auto it = bms_.find(typeId);
|
||||||
|
if (it != bms_.end()) it->second->setGroundZ(z);
|
||||||
|
ctrl_.view_.renderIncremental();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plane2DRenderStrategy::setBasemapKind(const std::string& typeId, int kind) {
|
||||||
|
bmKind_[typeId] = kind;
|
||||||
|
auto it = bms_.find(typeId);
|
||||||
|
if (it == bms_.end()) return;
|
||||||
|
if (kind == 0) it->second->show(0); // 矢量平面
|
||||||
|
else it->second->hide(); // 无(保留实例,仅隐瓦片)
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plane2DRenderStrategy::setBasemapOpacity(const std::string& typeId, double o) {
|
||||||
|
bmOpacity_[typeId] = o;
|
||||||
|
auto it = bms_.find(typeId);
|
||||||
|
if (it != bms_.end()) it->second->setOpacity(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plane2DRenderStrategy::createBasemap(const std::string& typeId) {
|
||||||
|
if (!basemapFactory_) return; // 工厂未注入(如纯逻辑单测,无 VTK) → 不建底图
|
||||||
|
// 平面 z:已建平面取 planeReg_(setPlaneZ 重建走此);onTypeActivated 时平面尚未建 → 以 zRefElev() 兜底
|
||||||
|
// (= add 即将写入的首勾平面 z,二者一致)。
|
||||||
|
const double gz =
|
||||||
|
planeReg_.hasPlane(typeId) ? planeReg_.planeZ(typeId) : ctrl_.view_.zRefElev();
|
||||||
|
auto bm = basemapFactory_(gz);
|
||||||
|
if (!bm) return;
|
||||||
|
const int kind = bmKind_.count(typeId) ? bmKind_[typeId] : 0;
|
||||||
|
const double op = bmOpacity_.count(typeId) ? bmOpacity_[typeId] : kDefaultBasemapOpacity;
|
||||||
|
bm->setOpacity(op); // 先定透明度,再 show 套用到初次铺瓦
|
||||||
|
if (kind == 0) bm->show(0); // 默认矢量平面;kind=1(无)则仅留实例
|
||||||
|
bms_[typeId] = std::move(bm); // 替换旧实例 → 旧底图适配器析构移除其瓦片
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::controller
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "controller/PlaneZRegistry.hpp"
|
||||||
|
|
||||||
|
namespace geopro::controller {
|
||||||
|
|
||||||
|
class VtkSceneController; // 策略委托回控制器既有渲染路径(add→addDatasetAsync/add2DDatasetAsync;remove→view.removeDataset)
|
||||||
|
|
||||||
|
// 平面底图抽象:控制器层不依赖 VTK/app(geopro_controller 仅链 geopro_data+Qt::Core)。
|
||||||
|
// app 层(main.cpp)经工厂注入具体 TileBasemap 适配器,仿既有 I3dSceneView/VtkSceneView 边界,
|
||||||
|
// 避免 geopro_controller 反向依赖 app 层与 VTK。
|
||||||
|
class IPlaneBasemap {
|
||||||
|
public:
|
||||||
|
virtual ~IPlaneBasemap() = default;
|
||||||
|
virtual void show(int kind) = 0; // 0=矢量平面(Street)/其它=无(hide)
|
||||||
|
virtual void hide() = 0;
|
||||||
|
virtual void setOpacity(double o) = 0; // 半透明度[0,1]
|
||||||
|
virtual void setGroundZ(double z) = 0; // 直接平移平面高程 z(拖 z 值滑块):改瓦片 position,无重铺
|
||||||
|
};
|
||||||
|
// 工厂:按平面 z 造一份平面底图(底图所需 scene/渲染窗/frame/数据半径规则由 app 闭包捕获)。
|
||||||
|
// 未注入(空)则不建底图——便于无 VTK 的纯逻辑单测。
|
||||||
|
using PlaneBasemapFactory = std::function<std::unique_ptr<IPlaneBasemap>(double groundZ)>;
|
||||||
|
|
||||||
|
class IDatasetRenderStrategy {
|
||||||
|
public:
|
||||||
|
virtual ~IDatasetRenderStrategy() = default;
|
||||||
|
virtual void add(const std::string& typeId, const std::string& dsId) = 0;
|
||||||
|
virtual void remove(const std::string& dsId) = 0;
|
||||||
|
virtual void onTypeActivated(const std::string& /*typeId*/) {}
|
||||||
|
virtual void onTypeDeactivated(const std::string& /*typeId*/) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class RenderStrategyRegistry {
|
||||||
|
public:
|
||||||
|
void registerStrategy(std::string id, std::unique_ptr<IDatasetRenderStrategy> s) {
|
||||||
|
strategies_[std::move(id)] = std::move(s);
|
||||||
|
}
|
||||||
|
IDatasetRenderStrategy* get(const std::string& id) const {
|
||||||
|
auto it = strategies_.find(id);
|
||||||
|
return it == strategies_.end() ? nullptr : it->second.get();
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
std::map<std::string, std::unique_ptr<IDatasetRenderStrategy>> strategies_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3 策略:各持 VtkSceneController& 引用,把 add/remove 委托回控制器既有渲染路径。
|
||||||
|
// Volume/Curtain::add 均转调 addDatasetAsync(其内部按 isVolumeDataset 自分体/帘面分支);
|
||||||
|
// Plane2D::add 暂转调 add2DDatasetAsync(Phase E/F 改平面 z + 底图)。remove 统一 view.removeDataset。
|
||||||
|
class VolumeRenderStrategy : public IDatasetRenderStrategy {
|
||||||
|
public:
|
||||||
|
explicit VolumeRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
|
||||||
|
void add(const std::string& typeId, const std::string& dsId) override;
|
||||||
|
void remove(const std::string& dsId) override;
|
||||||
|
private:
|
||||||
|
VtkSceneController& ctrl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CurtainRenderStrategy : public IDatasetRenderStrategy {
|
||||||
|
public:
|
||||||
|
explicit CurtainRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
|
||||||
|
void add(const std::string& typeId, const std::string& dsId) override;
|
||||||
|
void remove(const std::string& dsId) override;
|
||||||
|
private:
|
||||||
|
VtkSceneController& ctrl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 每个 2D 类型(段)的勾选足迹落到该类型「同一平面」上:首勾的 ds 定平面 z,后续投影到该 z;
|
||||||
|
// 全消则平面消失(z 遗忘)。平面 z 生命周期由 PlaneZRegistry 维护;足迹摆放经 add2DDatasetAsync(z)。
|
||||||
|
class Plane2DRenderStrategy : public IDatasetRenderStrategy {
|
||||||
|
public:
|
||||||
|
explicit Plane2DRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
|
||||||
|
void add(const std::string& typeId, const std::string& dsId) override;
|
||||||
|
void remove(const std::string& dsId) override;
|
||||||
|
void onTypeActivated(const std::string& typeId) override; // 首勾:建该类型平面矢量底图
|
||||||
|
void onTypeDeactivated(const std::string& typeId) override; // 全消:销毁该类型底图(瓦片随之消失)
|
||||||
|
// 滑块整体升降该类型平面 z:更新 planeReg_ 后,对该类型已勾选足迹移除并按新 z 重摆 + 底图重建于新 z。
|
||||||
|
void setPlaneZ(const std::string& typeId, double z);
|
||||||
|
// 底图工厂注入(main.cpp 构造后一次性下发;未注入则底图建造静默跳过,便于纯逻辑单测)。
|
||||||
|
void setBasemapFactory(PlaneBasemapFactory f) { basemapFactory_ = std::move(f); }
|
||||||
|
void setBasemapKind(const std::string& typeId, int kind); // 0=矢量平面(show)/1=无(hide)
|
||||||
|
void setBasemapOpacity(const std::string& typeId, double o); // 该类型底图半透明度[0,1]
|
||||||
|
private:
|
||||||
|
void createBasemap(const std::string& typeId); // 按当前 z/kind/opacity 建(或重建)该类型底图
|
||||||
|
|
||||||
|
VtkSceneController& ctrl_;
|
||||||
|
PlaneZRegistry planeReg_; // 按类型的平面 z 生命周期
|
||||||
|
std::map<std::string, std::string> dsToType_; // dsId→typeId(remove 只得 dsId,需自存反查)
|
||||||
|
|
||||||
|
// 每 2D 类型一份平面矢量底图(贴该类型平面 z);随平面建/销/升降。键=typeId。
|
||||||
|
std::map<std::string, std::unique_ptr<IPlaneBasemap>> bms_;
|
||||||
|
std::map<std::string, int> bmKind_; // 该类型底图选择(0=矢量平面/1=无),重建时复用
|
||||||
|
std::map<std::string, double> bmOpacity_; // 该类型底图透明度,重建时复用
|
||||||
|
PlaneBasemapFactory basemapFactory_; // app 注入的底图工厂(空=不建底图)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::controller
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/Field.hpp"
|
#include "model/Field.hpp"
|
||||||
|
|
@ -51,6 +52,9 @@ public:
|
||||||
// 2D 足迹:把测线/轨迹经纬折线平铺进 3D 地图(worldZ=摆放高程);按 dsId 跟踪以支持增量移除。
|
// 2D 足迹:把测线/轨迹经纬折线平铺进 3D 地图(worldZ=摆放高程);按 dsId 跟踪以支持增量移除。
|
||||||
virtual void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
virtual void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
|
||||||
double worldZ) = 0;
|
double worldZ) = 0;
|
||||||
|
// 直接平移一组 2D 足迹到新平面 z(拖 z 值滑块用):改足迹 actor 的 SetPosition,无移除+异步重载。
|
||||||
|
// 仅对属于足迹的 dsId 生效;即时渲染。默认空实现,测试 mock 无需覆盖。
|
||||||
|
virtual void setMapLinesZ(const std::vector<std::string>& /*dsIds*/, double /*z*/) {}
|
||||||
// 3D:DEM 地形 + 影像纹理。
|
// 3D:DEM 地形 + 影像纹理。
|
||||||
virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0;
|
virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0;
|
||||||
// 增量移除某数据集的全部图元(取消勾选时调,不影响其余 ds 与底图)。
|
// 增量移除某数据集的全部图元(取消勾选时调,不影响其余 ds 与底图)。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#pragma once
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace geopro::controller {
|
||||||
|
|
||||||
|
// 纯逻辑:按 2D 类型管理「平面 z + 成员集」。首勾定 z(之后固定); 全消则平面消失。见 spec §8.2。
|
||||||
|
// 无 Qt/VTK 依赖,便于纯逻辑单测。被 Plane2DRenderStrategy 持有以摆放足迹。
|
||||||
|
class PlaneZRegistry {
|
||||||
|
public:
|
||||||
|
// 某类型某 ds 勾选:首勾(成员集空)记录平面 z=dsZ;返回该类型当前平面 z(后续勾选投影到此)。
|
||||||
|
double onChecked(const std::string& typeId, const std::string& dsId, double dsZ) {
|
||||||
|
auto& p = planes_[typeId];
|
||||||
|
if (p.members.empty()) p.z = dsZ; // 首勾定 z
|
||||||
|
p.members.insert(dsId);
|
||||||
|
return p.z;
|
||||||
|
}
|
||||||
|
// 取消勾选:移出成员;该类型成员集空时清除条目(平面消失,z 遗忘)。
|
||||||
|
void onUnchecked(const std::string& typeId, const std::string& dsId) {
|
||||||
|
auto it = planes_.find(typeId);
|
||||||
|
if (it == planes_.end()) return;
|
||||||
|
it->second.members.erase(dsId);
|
||||||
|
if (it->second.members.empty()) planes_.erase(it); // 全消 → 平面消失
|
||||||
|
}
|
||||||
|
bool hasPlane(const std::string& typeId) const { return planes_.count(typeId) > 0; }
|
||||||
|
double planeZ(const std::string& typeId) const {
|
||||||
|
auto it = planes_.find(typeId);
|
||||||
|
return it == planes_.end() ? 0.0 : it->second.z;
|
||||||
|
}
|
||||||
|
// 滑块整体调:移动该类型既有平面 z(类型不存在则无操作)。
|
||||||
|
void setPlaneZ(const std::string& typeId, double z) {
|
||||||
|
auto it = planes_.find(typeId);
|
||||||
|
if (it != planes_.end()) it->second.z = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Plane {
|
||||||
|
double z = 0.0;
|
||||||
|
std::set<std::string> members;
|
||||||
|
};
|
||||||
|
std::map<std::string, Plane> planes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::controller
|
||||||
|
|
@ -9,21 +9,46 @@
|
||||||
|
|
||||||
#include "DatasetViewState.hpp"
|
#include "DatasetViewState.hpp"
|
||||||
#include "I3dSceneView.hpp"
|
#include "I3dSceneView.hpp"
|
||||||
|
#include "controller/DatasetRenderStrategy.hpp"
|
||||||
|
#include "repo/CategoryDescriptor.hpp"
|
||||||
#include "repo/IDatasetRepository.hpp"
|
#include "repo/IDatasetRepository.hpp"
|
||||||
|
|
||||||
namespace geopro::controller {
|
namespace geopro::controller {
|
||||||
|
|
||||||
namespace {
|
|
||||||
// 二维足迹「顶部/底部」摆放相对参考高程(Z=0)的偏移(米):控制器无地形/参考高程源
|
|
||||||
// (地形异步、帘面经纬未必到场),故退化为 Z=0 上/下固定偏移,使足迹不与帘面顶/底面重叠遮挡。
|
|
||||||
constexpr double kTopOffsetZ = 50.0; // 顶部:参考面上方
|
|
||||||
constexpr double kBottomOffsetZ = -50.0; // 底部:参考面下方
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
VtkSceneController::VtkSceneController(data::IDatasetRepository& dsRepo,
|
VtkSceneController::VtkSceneController(data::IDatasetRepository& dsRepo,
|
||||||
data::I3dSceneRepository& sceneRepo, I3dSceneView& view,
|
data::I3dSceneRepository& sceneRepo, I3dSceneView& view,
|
||||||
QObject* parent)
|
QObject* parent)
|
||||||
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {}
|
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {
|
||||||
|
// 注册 3 渲染策略(键与 CategoryDescriptor.renderStrategyId 对应)。各持本控制器引用,
|
||||||
|
// add/remove 委托回既有渲染路径(addDatasetAsync/add2DDatasetAsync/view_.removeDataset)。
|
||||||
|
registry_.registerStrategy("volume", std::make_unique<VolumeRenderStrategy>(*this));
|
||||||
|
registry_.registerStrategy("curtain", std::make_unique<CurtainRenderStrategy>(*this));
|
||||||
|
auto plane2d = std::make_unique<Plane2DRenderStrategy>(*this);
|
||||||
|
plane2d_ = plane2d.get(); // 留裸指针供 setPlaneZ 直呼(registry_ 持所有权)
|
||||||
|
registry_.registerStrategy("plane2d", std::move(plane2d));
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneController::setPlaneZ(const QString& typeId, double z) {
|
||||||
|
if (plane2d_) plane2d_->setPlaneZ(typeId.toStdString(), z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneController::setPlaneBasemapFactory(PlaneBasemapFactory factory) {
|
||||||
|
if (plane2d_) plane2d_->setBasemapFactory(std::move(factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneController::setBasemapKind(const QString& typeId, int kind) {
|
||||||
|
if (plane2d_) plane2d_->setBasemapKind(typeId.toStdString(), kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VtkSceneController::setBasemapOpacity(const QString& typeId, double opacity) {
|
||||||
|
if (plane2d_) plane2d_->setBasemapOpacity(typeId.toStdString(), opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
IDatasetRenderStrategy* VtkSceneController::strategyForType(const std::string& typeId) const {
|
||||||
|
for (const auto& d : geopro::data::categoryCatalog())
|
||||||
|
if (d.id == typeId) return registry_.get(d.renderStrategyId);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void VtkSceneController::setViewState(DatasetViewState* state) {
|
void VtkSceneController::setViewState(DatasetViewState* state) {
|
||||||
state_ = state;
|
state_ = state;
|
||||||
|
|
@ -58,111 +83,57 @@ void VtkSceneController::recolorDataset(const QString& qid) {
|
||||||
if (changed) view_.renderIncremental();
|
if (changed) view_.renderIncremental();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
|
void VtkSceneController::setCheckedDatasets(
|
||||||
std::vector<std::string> newDs;
|
const std::vector<std::pair<std::string, std::string>>& idType) {
|
||||||
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
|
std::map<std::string, std::string> next; // dsId→typeId
|
||||||
for (const QString& id : dsIds) newDs.push_back(id.toStdString());
|
for (const auto& p : idType) next[p.first] = p.second;
|
||||||
|
|
||||||
// 2D 俯视测线:保持全量重建(测线非按 ds 跟踪移除)。
|
const std::map<std::string, std::string> prev = checked_; // diff 快照(区分新增/移除)
|
||||||
if (mode_ == ViewMode::Map2D) {
|
|
||||||
checkedDs_ = std::move(newDs);
|
// 移除:旧有新无 → 派该 ds 类型策略 remove;活跃计数归零则 onTypeDeactivated。
|
||||||
rebuildInternal();
|
for (const auto& [id, typeId] : prev)
|
||||||
return;
|
if (!next.count(id)) {
|
||||||
|
if (auto* s = strategyForType(typeId)) s->remove(id);
|
||||||
|
if (--typeActive_[typeId] == 0)
|
||||||
|
if (auto* s = strategyForType(typeId)) s->onTypeDeactivated(typeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D:增量 diff —— 只处理新增/移除,不全量重建(底图、其余 ds、相机均不动)。
|
// 取景意图按「场景是否已有数据到场过」判定(连续快速勾选时 checked_ 已非空但首批未到场,
|
||||||
const std::set<std::string> oldSet(checkedDs_.begin(), checkedDs_.end());
|
// 不可据 checked_ 空否清取景意图,否则相机不对准数据 → 看似不渲染)。全消时复位基线。
|
||||||
const std::set<std::string> newSet(newDs.begin(), newDs.end());
|
|
||||||
|
|
||||||
for (const auto& id : checkedDs_)
|
|
||||||
if (!newSet.count(id)) view_.removeDataset(id); // 移除:旧有新无 → 仅删该 ds 图元
|
|
||||||
|
|
||||||
checkedDs_ = std::move(newDs);
|
|
||||||
// 取景意图按「场景是否已有数据到场过」判定,而非 checkedDs_ 是否空——否则连续快速勾选第二个
|
|
||||||
// ds 时 checkedDs_ 已非空但首批尚未到场,会被误清取景意图,相机不对准数据 → 看似不渲染。
|
|
||||||
fitOnArrival_ = !hadArrivedData_;
|
fitOnArrival_ = !hadArrivedData_;
|
||||||
// 仅当 3D 与 2D 都空才复位取景基线:否则取消全部 3D 但仍有 2D 足迹时,下个 3D 勾选会误跳取景。
|
if (next.empty()) hadArrivedData_ = false;
|
||||||
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
|
|
||||||
|
|
||||||
const unsigned long long gen = rebuildGeneration_; // 不自增:并发增量互不作废
|
// 先提交勾选集,再派 add:异步取数回调以 isChecked() 守「仍勾选?」,同步仓储(测试)会即时回灌,
|
||||||
for (const auto& id : checkedDs_)
|
// 若 add 时 checked_ 未更新则 isChecked 假、回调丢弃 → 不渲染。故必须先 commit 再 add。
|
||||||
if (!oldSet.count(id)) addDatasetAsync(id, gen); // 新增:新有旧无 → 异步取数增量入场
|
checked_ = next;
|
||||||
|
|
||||||
|
// 新增:新有旧无 → 活跃计数 0→1 时 onTypeActivated;再派策略 add(委托回既有渲染路径)。
|
||||||
|
for (const auto& [id, typeId] : checked_)
|
||||||
|
if (!prev.count(id)) {
|
||||||
|
if (typeActive_[typeId]++ == 0)
|
||||||
|
if (auto* s = strategyForType(typeId)) s->onTypeActivated(typeId);
|
||||||
|
if (auto* s = strategyForType(typeId)) s->add(typeId, id);
|
||||||
|
}
|
||||||
|
|
||||||
view_.renderIncremental(); // 立即反映移除 / 触发坐标轴重算
|
view_.renderIncremental(); // 立即反映移除 / 触发坐标轴重算
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) {
|
void VtkSceneController::add2DDatasetAsync(const std::string& dsId, unsigned long long gen,
|
||||||
std::vector<std::string> newDs;
|
double z) {
|
||||||
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
|
|
||||||
for (const QString& id : dsIds) newDs.push_back(id.toStdString());
|
|
||||||
|
|
||||||
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff(不全量重建,不打断 3D 帘面/体)。
|
|
||||||
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
|
|
||||||
const std::set<std::string> newSet(newDs.begin(), newDs.end());
|
|
||||||
|
|
||||||
for (const auto& id : checked2dDs_)
|
|
||||||
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
|
|
||||||
|
|
||||||
checked2dDs_ = std::move(newDs);
|
|
||||||
// 取景基线与 3D 路径统一用 hadArrivedData_(而非"两栏皆空"):否则二维分析下若已有隐藏的 3D 数据,
|
|
||||||
// 勾选首条足迹会因 wasEmpty=false 而不取景 → 足迹落在视野外。切 tab 时 onAnalysisModeChanged 已按
|
|
||||||
// 目标维度是否有数据重置该基线,故此处首条可见维度数据能正确取景。
|
|
||||||
fitOnArrival_ = !hadArrivedData_;
|
|
||||||
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
|
|
||||||
|
|
||||||
// 足迹画进 View3D 场景;mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
|
|
||||||
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
|
|
||||||
const unsigned long long gen = rebuildGeneration_; // 不自增:与 3D 增量互不作废
|
|
||||||
for (const auto& id : checked2dDs_)
|
|
||||||
if (!oldSet.count(id)) add2DDatasetAsync(id, gen); // 新增 → 异步取足迹增量入场
|
|
||||||
}
|
|
||||||
|
|
||||||
view_.renderIncremental(); // 立即反映移除
|
|
||||||
}
|
|
||||||
|
|
||||||
void VtkSceneController::set2DPlacement(int mode, double customZ) {
|
|
||||||
const bool changed = (mode != placement2dMode_) || (mode == 4 && customZ != customZ2d_);
|
|
||||||
placement2dMode_ = mode;
|
|
||||||
customZ2d_ = customZ;
|
|
||||||
if (!changed || checked2dDs_.empty()) return;
|
|
||||||
|
|
||||||
// 摆放变化 → 对已勾选足迹重摆:先全部移除,再按新 Z 重加(mode=0 关闭则只移除不重加)。
|
|
||||||
for (const auto& id : checked2dDs_) view_.removeDataset(id);
|
|
||||||
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
|
|
||||||
const unsigned long long gen = rebuildGeneration_;
|
|
||||||
fitOnArrival_ = false; // 重摆:保持相机
|
|
||||||
for (const auto& id : checked2dDs_) add2DDatasetAsync(id, gen);
|
|
||||||
}
|
|
||||||
view_.renderIncremental();
|
|
||||||
}
|
|
||||||
|
|
||||||
double VtkSceneController::placementZ() const {
|
|
||||||
const double surf = view_.zRefElev(); // 真实地表高程基准(测线地表高程)
|
|
||||||
switch (placement2dMode_) {
|
|
||||||
case 1: return 0.0; // Z=0(世界原点)
|
|
||||||
case 2: return surf + kTopOffsetZ; // 顶部:贴真实地表上方
|
|
||||||
case 3: return surf + kBottomOffsetZ; // 底部:真实地表下方
|
|
||||||
case 4: return customZ2d_; // 自定义
|
|
||||||
default: return 0.0; // 关闭(0) 不应走到此(调用方拦截)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VtkSceneController::add2DDatasetAsync(const std::string& dsId, unsigned long long gen) {
|
|
||||||
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
|
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
|
||||||
loadingDs_.insert(dsId);
|
loadingDs_.insert(dsId);
|
||||||
QPointer<VtkSceneController> self(this);
|
QPointer<VtkSceneController> self(this);
|
||||||
sceneRepo_.loadMapLine(
|
sceneRepo_.loadMapLine(
|
||||||
dsId,
|
dsId,
|
||||||
[self, gen, dsId](data::MapLine line) {
|
[self, gen, dsId, z](data::MapLine line) {
|
||||||
if (!self) return;
|
if (!self) return;
|
||||||
self->loadingDs_.erase(dsId);
|
self->loadingDs_.erase(dsId);
|
||||||
// gen 作废 / 已取消勾选 / 摆放已关闭 → 丢弃迟到回调。
|
// gen 作废 / 已取消勾选 → 丢弃迟到回调。
|
||||||
if (gen != self->rebuildGeneration_ || !self->is2DChecked(dsId) ||
|
if (gen != self->rebuildGeneration_ || !self->is2DChecked(dsId)) {
|
||||||
self->placement2dMode_ == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 落地时按当前摆放 Z(非请求时快照)→ 加载期间摆放变化也取最新高程。
|
// 足迹摆到所属 2D 类型的平面 z(首勾定、后续投影;由 Plane2DRenderStrategy 决定)。
|
||||||
self->view_.addMapLine(dsId, line, self->placementZ());
|
self->view_.addMapLine(dsId, line, z);
|
||||||
self->onDatasetArrived();
|
self->onDatasetArrived();
|
||||||
},
|
},
|
||||||
[self, gen, dsId](const std::string& m) {
|
[self, gen, dsId](const std::string& m) {
|
||||||
|
|
@ -260,11 +231,11 @@ void VtkSceneController::onDatasetArrived() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VtkSceneController::isChecked(const std::string& dsId) const {
|
bool VtkSceneController::isChecked(const std::string& dsId) const {
|
||||||
return std::find(checkedDs_.begin(), checkedDs_.end(), dsId) != checkedDs_.end();
|
return checked_.count(dsId) > 0; // 统一勾选集(异步回调「仍勾选?」守护)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VtkSceneController::is2DChecked(const std::string& dsId) const {
|
bool VtkSceneController::is2DChecked(const std::string& dsId) const {
|
||||||
return std::find(checked2dDs_.begin(), checked2dDs_.end(), dsId) != checked2dDs_.end();
|
return checked_.count(dsId) > 0; // 同上:2D 足迹与 3D 同处统一勾选集
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneController::setViewMode(ViewMode mode) {
|
void VtkSceneController::setViewMode(ViewMode mode) {
|
||||||
|
|
@ -272,14 +243,6 @@ void VtkSceneController::setViewMode(ViewMode mode) {
|
||||||
rebuildInternal();
|
rebuildInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneController::onAnalysisModeChanged(bool is2D) {
|
|
||||||
// 切「三维分析/二维分析」tab:按目标维度是否已有数据重置取景基线。
|
|
||||||
// 目标维度空 → hadArrivedData_=false:切换后该维度第一条数据自动取景(治"3D 数据不知生成到哪")。
|
|
||||||
// 目标维度非空 → hadArrivedData_=true:视图切换时已 fit 到该维度,后续勾选不再跳(与三维一致)。
|
|
||||||
// 显隐/相机/坐标轴由 VtkSceneView::setAnalysisMode2D 处理(上层在同一处调用);此处只管取景基线。
|
|
||||||
hadArrivedData_ = is2D ? !checked2dDs_.empty() : !checkedDs_.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VtkSceneController::setLayer(SceneLayer layer, bool on) {
|
void VtkSceneController::setLayer(SceneLayer layer, bool on) {
|
||||||
switch (layer) {
|
switch (layer) {
|
||||||
case SceneLayer::Curtain: showCurtain_ = on; break;
|
case SceneLayer::Curtain: showCurtain_ = on; break;
|
||||||
|
|
@ -365,12 +328,6 @@ void VtkSceneController::zoomIn() { view_.zoom(1.2); }
|
||||||
void VtkSceneController::zoomOut() { view_.zoom(1.0 / 1.2); }
|
void VtkSceneController::zoomOut() { view_.zoom(1.0 / 1.2); }
|
||||||
void VtkSceneController::fit() { view_.fitView(); }
|
void VtkSceneController::fit() { view_.fitView(); }
|
||||||
|
|
||||||
const geopro::core::Grid& VtkSceneController::grid(const std::string& dsId) {
|
|
||||||
auto it = gridCache_.find(dsId);
|
|
||||||
if (it == gridCache_.end()) it = gridCache_.emplace(dsId, dsRepo_.loadGrid(dsId)).first;
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string& dsId) {
|
const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string& dsId) {
|
||||||
auto it = colorScaleCache_.find(dsId);
|
auto it = colorScaleCache_.find(dsId);
|
||||||
if (it == colorScaleCache_.end())
|
if (it == colorScaleCache_.end())
|
||||||
|
|
@ -380,7 +337,6 @@ const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string
|
||||||
|
|
||||||
void VtkSceneController::rebuildInternal() {
|
void VtkSceneController::rebuildInternal() {
|
||||||
const unsigned long long gen = ++rebuildGeneration_; // 自增:作废此前所有在途增量回调
|
const unsigned long long gen = ++rebuildGeneration_; // 自增:作废此前所有在途增量回调
|
||||||
const bool is2D = (mode_ == ViewMode::Map2D);
|
|
||||||
|
|
||||||
view_.clear(); // 移除全部数据图元(保留底图);frame 重锚标志复位
|
view_.clear(); // 移除全部数据图元(保留底图);frame 重锚标志复位
|
||||||
loadingDs_.clear(); // 旧在途加载随之作废(回调按 gen 丢弃)
|
loadingDs_.clear(); // 旧在途加载随之作废(回调按 gen 丢弃)
|
||||||
|
|
@ -392,9 +348,6 @@ void VtkSceneController::rebuildInternal() {
|
||||||
|
|
||||||
// 坏 dsId(loadGrid/loadColorScale 抛异常)= best-effort 跳过:emit loadFailed 但不中断。
|
// 坏 dsId(loadGrid/loadColorScale 抛异常)= best-effort 跳过:emit loadFailed 但不中断。
|
||||||
try {
|
try {
|
||||||
if (is2D) {
|
|
||||||
for (const auto& dsId : checkedDs_) view_.addSurveyLine(grid(dsId));
|
|
||||||
} else {
|
|
||||||
// 回调用 QPointer<self> 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。
|
// 回调用 QPointer<self> 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。
|
||||||
QPointer<VtkSceneController> self(this);
|
QPointer<VtkSceneController> self(this);
|
||||||
if (showTerrain_) {
|
if (showTerrain_) {
|
||||||
|
|
@ -409,17 +362,16 @@ void VtkSceneController::rebuildInternal() {
|
||||||
emit self->loadFailed(QString::fromStdString(m));
|
emit self->loadFailed(QString::fromStdString(m));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const auto& dsId : checkedDs_) addDatasetAsync(dsId, gen);
|
// 全量重建:clear 已移除全部图元,据统一勾选集经各 ds 类型策略重放 add(不动活跃计数:
|
||||||
// 二维足迹随全量重建一并重画(clear 已移除其图元);mode=0 关闭则跳过。
|
// 已在 setCheckedDatasets 计入;策略 add 内部转调 addDatasetAsync/add2DDatasetAsync)。
|
||||||
if (placement2dMode_ != 0)
|
for (const auto& [dsId, typeId] : checked_)
|
||||||
for (const auto& dsId : checked2dDs_) add2DDatasetAsync(dsId, gen);
|
if (auto* s = strategyForType(typeId)) s->add(typeId, dsId);
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
emit loadFailed(QString::fromStdString(e.what()));
|
emit loadFailed(QString::fromStdString(e.what()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保留相机重建(改VE):不 ResetCamera,原地按新夸张重绘。
|
// 保留相机重建(改VE):不 ResetCamera,原地按新夸张重绘。视图恒三维(is2D=false)。
|
||||||
view_.render(is2D, /*resetCamera=*/!preserveCameraOnRebuild_);
|
view_.render(/*is2D=*/false, /*resetCamera=*/!preserveCameraOnRebuild_);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::controller
|
} // namespace geopro::controller
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "I3dSceneView.hpp"
|
#include "I3dSceneView.hpp"
|
||||||
|
#include "controller/DatasetRenderStrategy.hpp"
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/Field.hpp"
|
#include "model/Field.hpp"
|
||||||
#include "repo/I3dSceneRepository.hpp"
|
#include "repo/I3dSceneRepository.hpp"
|
||||||
|
|
@ -21,8 +24,8 @@ namespace geopro::controller {
|
||||||
|
|
||||||
class DatasetViewState; // 跨视图共享色阶真源(统一同步机制)
|
class DatasetViewState; // 跨视图共享色阶真源(统一同步机制)
|
||||||
|
|
||||||
// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。
|
// 中央视图模式:固定三维视图(帘面/体素/地形)。旧二维俯视测线(Map2D)路径已退役(main 恒 View3D)。
|
||||||
enum class ViewMode { Map2D, View3D };
|
enum class ViewMode { View3D };
|
||||||
|
|
||||||
// 三维图层("视图详情"浮层勾选)。
|
// 三维图层("视图详情"浮层勾选)。
|
||||||
enum class SceneLayer { Curtain, Voxel, Terrain };
|
enum class SceneLayer { Curtain, Voxel, Terrain };
|
||||||
|
|
@ -34,6 +37,10 @@ enum class SceneLayer { Curtain, Voxel, Terrain };
|
||||||
// 不持有 widget;不认 vtkActor/vtkVolume(全交给 I3dSceneView)。
|
// 不持有 widget;不认 vtkActor/vtkVolume(全交给 I3dSceneView)。
|
||||||
class VtkSceneController : public QObject {
|
class VtkSceneController : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
// 渲染策略委托回控制器既有路径(addDatasetAsync/add2DDatasetAsync/view_);友元免widen公有面。
|
||||||
|
friend class VolumeRenderStrategy;
|
||||||
|
friend class CurtainRenderStrategy;
|
||||||
|
friend class Plane2DRenderStrategy;
|
||||||
public:
|
public:
|
||||||
VtkSceneController(data::IDatasetRepository& dsRepo, data::I3dSceneRepository& sceneRepo,
|
VtkSceneController(data::IDatasetRepository& dsRepo, data::I3dSceneRepository& sceneRepo,
|
||||||
I3dSceneView& view, QObject* parent = nullptr);
|
I3dSceneView& view, QObject* parent = nullptr);
|
||||||
|
|
@ -42,16 +49,18 @@ public:
|
||||||
// 构造后由 main.cpp 注入一次。
|
// 构造后由 main.cpp 注入一次。
|
||||||
void setViewState(DatasetViewState* state);
|
void setViewState(DatasetViewState* state);
|
||||||
|
|
||||||
|
// 注入 2D 平面底图工厂(app 层闭包捕获 scene/渲染窗/frame/数据半径规则,造 TileBasemap 适配器):
|
||||||
|
// 转交 Plane2DRenderStrategy,供各类型平面按需建底图。main.cpp 构造后一次性下发。
|
||||||
|
void setPlaneBasemapFactory(PlaneBasemapFactory factory);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// 勾选并集统一入口(取代旧 setCheckedDatasets(QStringList)/setChecked2DDatasets):
|
||||||
|
// 每项 = (dsId, typeId=描述符 id)。diff vs 上次后按 catalog[typeId].renderStrategyId 派给策略
|
||||||
|
// add/remove,并维护「每 typeId 活跃数」在首勾/全消时调 onTypeActivated/Deactivated。
|
||||||
|
void setCheckedDatasets(const std::vector<std::pair<std::string, std::string>>& idType);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setCheckedDatasets(const QStringList& dsIds);
|
|
||||||
// 二维数据集栏勾选(足迹型测线/轨迹)→ 平铺进 View3D 地图。与 3D 勾选集独立,按 dsId 增量。
|
|
||||||
void setChecked2DDatasets(const QStringList& dsIds);
|
|
||||||
// 二维足迹摆放高度(mode:0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义;customZ 仅 mode=4 用)。
|
|
||||||
void set2DPlacement(int mode, double customZ);
|
|
||||||
void setViewMode(ViewMode mode);
|
void setViewMode(ViewMode mode);
|
||||||
// 切「三维分析/二维分析」tab(A 期):按目标维度是否已有数据重置取景基线,使切换后该维度第一条
|
|
||||||
// 数据自动取景。显隐/相机/坐标轴由 VtkSceneView::setAnalysisMode2D 负责(上层在同一处一并调用)。
|
|
||||||
void onAnalysisModeChanged(bool is2D);
|
|
||||||
void setLayer(SceneLayer layer, bool on);
|
void setLayer(SceneLayer layer, bool on);
|
||||||
void setVerticalExaggeration(double ve);
|
void setVerticalExaggeration(double ve);
|
||||||
// 三维体透明度调节(工具条滑块):运行时更新已渲染体的不透明度,并作为后续新体默认(0~1)。
|
// 三维体透明度调节(工具条滑块):运行时更新已渲染体的不透明度,并作为后续新体默认(0~1)。
|
||||||
|
|
@ -71,6 +80,12 @@ public slots:
|
||||||
// 坐标轴设置面板「应用」:一次性下发 显示方式 + 单位 + per-axis 可见性/范围(单次重建)。
|
// 坐标轴设置面板「应用」:一次性下发 显示方式 + 单位 + per-axis 可见性/范围(单次重建)。
|
||||||
void setAxesConfig(AxesMode mode, AxesUnit unit, const AxisRangeCfg& x, const AxisRangeCfg& y,
|
void setAxesConfig(AxesMode mode, AxesUnit unit, const AxisRangeCfg& x, const AxisRangeCfg& y,
|
||||||
const AxisRangeCfg& z);
|
const AxisRangeCfg& z);
|
||||||
|
// 2D 段「z 值」滑块:整体升降某 2D 类型平面(含其上全部已勾选足迹)。转交 Plane2DRenderStrategy。
|
||||||
|
void setPlaneZ(const QString& typeId, double z);
|
||||||
|
// 2D 段「底图」弹窗:切该类型平面底图 矢量平面(0)/无(1) + 透明度[0,1]。转交 Plane2DRenderStrategy。
|
||||||
|
void setBasemapKind(const QString& typeId, int kind);
|
||||||
|
void setBasemapOpacity(const QString& typeId, double opacity);
|
||||||
|
|
||||||
void applyView(ViewDir dir); // 6 向快捷视图
|
void applyView(ViewDir dir); // 6 向快捷视图
|
||||||
void zoomIn(); // Zoom In (×1.2)
|
void zoomIn(); // Zoom In (×1.2)
|
||||||
void zoomOut(); // Zoom Out (×1/1.2)
|
void zoomOut(); // Zoom Out (×1/1.2)
|
||||||
|
|
@ -91,28 +106,27 @@ private:
|
||||||
void recolorDataset(const QString& dsId);
|
void recolorDataset(const QString& dsId);
|
||||||
// 增量加入单个 ds(帘面/体素,按图层开关);回调按 gen + 仍勾选 守护,落地后增量渲染。
|
// 增量加入单个 ds(帘面/体素,按图层开关);回调按 gen + 仍勾选 守护,落地后增量渲染。
|
||||||
void addDatasetAsync(const std::string& dsId, unsigned long long gen);
|
void addDatasetAsync(const std::string& dsId, unsigned long long gen);
|
||||||
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 当前摆放 Z);回调按 gen + 仍勾选 守护。
|
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 摆放 z);回调按 gen + 仍勾选 守护。
|
||||||
void add2DDatasetAsync(const std::string& dsId, unsigned long long gen);
|
// z 为该 ds 所属 2D 类型的平面高程(由 Plane2DRenderStrategy 经 PlaneZRegistry 决定,§E2)。
|
||||||
|
void add2DDatasetAsync(const std::string& dsId, unsigned long long gen, double z);
|
||||||
void onDatasetArrived(); // 单个 ds 落地后:增量渲染 + 首批数据自动取景
|
void onDatasetArrived(); // 单个 ds 落地后:增量渲染 + 首批数据自动取景
|
||||||
bool isChecked(const std::string& dsId) const;
|
bool isChecked(const std::string& dsId) const;
|
||||||
bool is2DChecked(const std::string& dsId) const;
|
bool is2DChecked(const std::string& dsId) const;
|
||||||
// 当前摆放模式下足迹的世界 Z(mode 0=关闭由调用方拦截;此处算 1/2/3/4 的 Z)。
|
// 按 typeId 查其渲染策略(catalog[typeId].renderStrategyId → registry_)。未知 typeId 返回 nullptr。
|
||||||
double placementZ() const;
|
IDatasetRenderStrategy* strategyForType(const std::string& typeId) const;
|
||||||
|
|
||||||
data::IDatasetRepository& dsRepo_;
|
data::IDatasetRepository& dsRepo_;
|
||||||
data::I3dSceneRepository& sceneRepo_;
|
data::I3dSceneRepository& sceneRepo_;
|
||||||
I3dSceneView& view_;
|
I3dSceneView& view_;
|
||||||
|
|
||||||
std::vector<std::string> checkedDs_;
|
// 统一勾选集(2D+3D 合一):dsId→typeId(描述符 id)。增量 diff 的真源;rebuildInternal 据此重放。
|
||||||
// 二维足迹勾选集(与 checkedDs_ 独立;都画进 View3D 场景)。按 dsId 增量加/删。
|
std::map<std::string, std::string> checked_;
|
||||||
std::vector<std::string> checked2dDs_;
|
// 每 typeId 活跃计数:首勾(0→1)调 onTypeActivated、全消(1→0)调 onTypeDeactivated。
|
||||||
// 二维足迹摆放:mode 0关闭/1 Z=0/2顶部/3底部/4自定义;customZ2d_ 仅 mode=4 用。
|
std::map<std::string, int> typeActive_;
|
||||||
// 默认 Z=0(1) 与 Column2DDataset「2D视图」下拉可见默认项一致——避免「下拉显示 Z=0 但
|
// 渲染策略注册表(构造时注册 volume/curtain/plane2d 三策略,各持本控制器引用)。
|
||||||
// 控制器实为关闭」的初始信号丢失desync(组合框 setCurrentIndex 在 connect 前发射、且
|
RenderStrategyRegistry registry_;
|
||||||
// 组件早于 main.cpp 接线构造,初始 view2DModeChanged 永不送达),致勾选足迹静默不渲染。
|
Plane2DRenderStrategy* plane2d_ = nullptr; // registry_ 中 plane2d 策略的裸指针(setPlaneZ 免下转型)
|
||||||
int placement2dMode_ = 1;
|
ViewMode mode_ = ViewMode::View3D;
|
||||||
double customZ2d_ = 0.0;
|
|
||||||
ViewMode mode_ = ViewMode::Map2D;
|
|
||||||
bool showCurtain_ = true;
|
bool showCurtain_ = true;
|
||||||
bool showVoxel_ = false;
|
bool showVoxel_ = false;
|
||||||
bool showTerrain_ = false;
|
bool showTerrain_ = false;
|
||||||
|
|
@ -128,9 +142,8 @@ private:
|
||||||
QPointer<DatasetViewState> state_; // 跨视图色阶真源(注入;编辑/加载/重着色都经它;QPointer 防悬挂)
|
QPointer<DatasetViewState> state_; // 跨视图色阶真源(注入;编辑/加载/重着色都经它;QPointer 防悬挂)
|
||||||
|
|
||||||
// 缓存(按 dsId):避免重复读盘/插值。
|
// 缓存(按 dsId):避免重复读盘/插值。
|
||||||
std::map<std::string, geopro::core::Grid> gridCache_;
|
|
||||||
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
|
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
|
||||||
// 帘面源网格缓存:帘面重着色需 grid 重建 addCurtain(loadSection 的 s.grid 不在 gridCache_)。
|
// 帘面源网格缓存:帘面重着色需 grid 重建 addCurtain(loadSection 的 s.grid 缓存于此)。
|
||||||
std::map<std::string, geopro::core::Grid> sectionGridCache_;
|
std::map<std::string, geopro::core::Grid> sectionGridCache_;
|
||||||
std::map<std::string, data::VolumeGrid> volumeCache_;
|
std::map<std::string, data::VolumeGrid> volumeCache_;
|
||||||
// 三维体色阶缓存:mock 体在 dsRepo_ 无条目,色阶随 loadVolume 一起交付并缓存于此。
|
// 三维体色阶缓存:mock 体在 dsRepo_ 无条目,色阶随 loadVolume 一起交付并缓存于此。
|
||||||
|
|
@ -147,7 +160,6 @@ private:
|
||||||
// 正在加载的 ds:防重复勾选竞态重复请求;全量重建时清空。
|
// 正在加载的 ds:防重复勾选竞态重复请求;全量重建时清空。
|
||||||
std::set<std::string> loadingDs_;
|
std::set<std::string> loadingDs_;
|
||||||
|
|
||||||
const geopro::core::Grid& grid(const std::string& dsId);
|
|
||||||
const geopro::core::ColorScale& colorScale(const std::string& dsId);
|
const geopro::core::ColorScale& colorScale(const std::string& dsId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
#include "io/gpr/GpsTrack.hpp"
|
#include "io/gpr/GpsTrack.hpp"
|
||||||
#include "io/gpr/IprHeader.hpp"
|
#include "io/gpr/IprHeader.hpp"
|
||||||
#include "io/gpr/IprbReader.hpp"
|
#include "io/gpr/IprbReader.hpp"
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ constexpr double kPi = 3.14159265358979323846;
|
||||||
|
|
||||||
// 读 .iprh 文本 → 解析头(与 .iprb 同名)。
|
// 读 .iprh 文本 → 解析头(与 .iprb 同名)。
|
||||||
geopro::io::gpr::IprHeader readHeaderFor(const std::string& iprbPath) {
|
geopro::io::gpr::IprHeader readHeaderFor(const std::string& iprbPath) {
|
||||||
fs::path h = fs::path(iprbPath).replace_extension(".iprh");
|
fs::path h = geopro::io::gpr::localPath(iprbPath).replace_extension(".iprh");
|
||||||
std::ifstream f(h);
|
std::ifstream f(h);
|
||||||
if (!f) throw std::runtime_error("GeoVolumeBuilder: 打不开 iprh " + h.string());
|
if (!f) throw std::runtime_error("GeoVolumeBuilder: 打不开 iprh " + h.string());
|
||||||
std::string text((std::istreambuf_iterator<char>(f)),
|
std::string text((std::istreambuf_iterator<char>(f)),
|
||||||
|
|
@ -55,7 +56,7 @@ std::int64_t totalTracesOf(const std::vector<std::string>& iprb, int samples) {
|
||||||
const std::int64_t per = static_cast<std::int64_t>(samples) * 2;
|
const std::int64_t per = static_cast<std::int64_t>(samples) * 2;
|
||||||
if (per <= 0) throw std::runtime_error("samples<=0");
|
if (per <= 0) throw std::runtime_error("samples<=0");
|
||||||
for (const auto& p : iprb) {
|
for (const auto& p : iprb) {
|
||||||
const std::int64_t bytes = static_cast<std::int64_t>(fs::file_size(p));
|
const std::int64_t bytes = static_cast<std::int64_t>(fs::file_size(geopro::io::gpr::localPath(p)));
|
||||||
minTr = std::min(minTr, bytes / per);
|
minTr = std::min(minTr, bytes / per);
|
||||||
}
|
}
|
||||||
return minTr;
|
return minTr;
|
||||||
|
|
@ -137,7 +138,7 @@ GeoBuildResult buildGeoVolume(const std::vector<GeoLineInput>& lines,
|
||||||
for (const auto& p : tracks[i].pts)
|
for (const auto& p : tracks[i].pts)
|
||||||
sc.trackM.push_back(lonLatToLocalM(p.lat, p.lon, minLat, minLon));
|
sc.trackM.push_back(lonLatToLocalM(p.lat, p.lon, minLat, minLon));
|
||||||
|
|
||||||
std::ifstream ordF(lines[i].ord);
|
std::ifstream ordF(geopro::io::gpr::localPath(lines[i].ord));
|
||||||
if (!ordF) throw std::runtime_error("buildGeoVolume: 打不开 ord " + lines[i].ord);
|
if (!ordF) throw std::runtime_error("buildGeoVolume: 打不开 ord " + lines[i].ord);
|
||||||
std::string ordText((std::istreambuf_iterator<char>(ordF)),
|
std::string ordText((std::istreambuf_iterator<char>(ordF)),
|
||||||
std::istreambuf_iterator<char>());
|
std::istreambuf_iterator<char>());
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ add_library(geopro_data STATIC
|
||||||
repo/LocalSampleRepository.cpp
|
repo/LocalSampleRepository.cpp
|
||||||
repo/LocalSample3dRepository.cpp
|
repo/LocalSample3dRepository.cpp
|
||||||
repo/DatasetFieldDictionary.cpp
|
repo/DatasetFieldDictionary.cpp
|
||||||
|
repo/CategoryDescriptor.cpp
|
||||||
dto/NavDto.cpp
|
dto/NavDto.cpp
|
||||||
dto/Vtk3dRequests.cpp
|
dto/Vtk3dRequests.cpp
|
||||||
dto/DatasetChartDto.cpp
|
dto/DatasetChartDto.cpp
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
#include "data/store/ChunkedVolumeStore.hpp"
|
#include "data/store/ChunkedVolumeStore.hpp"
|
||||||
#include "io/gpr/GprSurveyAssembler.hpp"
|
#include "io/gpr/GprSurveyAssembler.hpp"
|
||||||
#include "io/gpr/IprHeader.hpp"
|
#include "io/gpr/IprHeader.hpp"
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ std::string toHeaderPath(const std::string& iprbPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string readFileText(const std::string& path) {
|
std::string readFileText(const std::string& path) {
|
||||||
std::ifstream f(path, std::ios::binary);
|
std::ifstream f(geopro::io::gpr::localPath(path), std::ios::binary);
|
||||||
if (!f) throw std::runtime_error("StreamingVolumeBuilder: 无法打开 " + path);
|
if (!f) throw std::runtime_error("StreamingVolumeBuilder: 无法打开 " + path);
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
ss << f.rdbuf();
|
ss << f.rdbuf();
|
||||||
|
|
@ -68,7 +69,7 @@ std::int64_t totalTraces(const std::vector<std::string>& iprb, double& surveyDx)
|
||||||
throw std::runtime_error("StreamingVolumeBuilder: samples<=0");
|
throw std::runtime_error("StreamingVolumeBuilder: samples<=0");
|
||||||
if (c == 0) surveyDx = h.distanceInterval;
|
if (c == 0) surveyDx = h.distanceInterval;
|
||||||
const std::int64_t bytes =
|
const std::int64_t bytes =
|
||||||
static_cast<std::int64_t>(fs::file_size(fs::path(iprb[c])));
|
static_cast<std::int64_t>(fs::file_size(geopro::io::gpr::localPath(iprb[c])));
|
||||||
const std::int64_t per = static_cast<std::int64_t>(h.samples) * 2;
|
const std::int64_t per = static_cast<std::int64_t>(h.samples) * 2;
|
||||||
if (per <= 0 || bytes % per != 0)
|
if (per <= 0 || bytes % per != 0)
|
||||||
throw std::runtime_error("StreamingVolumeBuilder: .iprb 字节非整道");
|
throw std::runtime_error("StreamingVolumeBuilder: .iprb 字节非整道");
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,11 @@ bool Api3dRepository::volumeInfo(const std::string& dsId, VolumeInfo& out) const
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Api3dRepository::isRadarVolume(const std::string& dsId) const {
|
||||||
|
auto it = volumes_.find(dsId);
|
||||||
|
return it != volumes_.end() && !it->second.linePrefix.empty();
|
||||||
|
}
|
||||||
|
|
||||||
void Api3dRepository::appendGridPoints(const core::Grid& g, core::PointSet& pts) const {
|
void Api3dRepository::appendGridPoints(const core::Grid& g, core::PointSet& pts) const {
|
||||||
const int nx = g.nx(), ny = g.ny();
|
const int nx = g.nx(), ny = g.ny();
|
||||||
if (nx < 1 || ny < 1 || g.y.size() < static_cast<std::size_t>(ny)) return;
|
if (nx < 1 || ny < 1 || g.y.size() < static_cast<std::size_t>(ny)) return;
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@ public:
|
||||||
};
|
};
|
||||||
// 取回三维体详情;dsId 非三维体返回 false(不弹空对话框)。
|
// 取回三维体详情;dsId 非三维体返回 false(不弹空对话框)。
|
||||||
bool volumeInfo(const std::string& dsId, VolumeInfo& out) const;
|
bool volumeInfo(const std::string& dsId, VolumeInfo& out) const;
|
||||||
|
// 该 dsId 是否为雷达三维体(StoredVolume 存在且 linePrefix 非空 → 走 loadVolume 雷达懒建分支)。
|
||||||
|
// 用于「沿线位置」滑块门控:仅实际渲染了雷达体时显示,不再靠数据包围盒细长度误判。
|
||||||
|
bool isRadarVolume(const std::string& dsId) const;
|
||||||
// 已保存切片的列表行(ddCode="dd_slice",parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。
|
// 已保存切片的列表行(ddCode="dd_slice",parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。
|
||||||
std::vector<DsRow> sliceRows() const;
|
std::vector<DsRow> sliceRows() const;
|
||||||
// 异常列表行(ddCode="dd_anomaly",parentId=remarkSourceId=归属实体[体/切片] dsId → 三级树自动挂载),
|
// 异常列表行(ddCode="dd_anomaly",parentId=remarkSourceId=归属实体[体/切片] dsId → 三级树自动挂载),
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace geopro::app {
|
|
||||||
|
|
||||||
// 一个数据类型大类段的配置(spec §5)。识别键二选一:dsTypeCode 优先;ddCode 用于三维体/切片。
|
|
||||||
struct CategorySpec {
|
|
||||||
std::string id; // 段稳定 id
|
|
||||||
std::string title; // 段标题(UI 显示)
|
|
||||||
std::string dsTypeCode; // 主识别键(空=不按 dsTypeCode)
|
|
||||||
std::string ddCode; // 次识别键(dd_voxel/dd_slice;空=不按 ddCode)
|
|
||||||
bool canGenerateVolume; // 段内是否提供「生成三维体」入口(仅反演类)
|
|
||||||
bool hasArrayTypeFilter; // 段头是否显示装置类型筛选(仅 ERT 类)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5 段固定有序(spec §5 表)。
|
|
||||||
inline const std::vector<CategorySpec>& categoryConfigs() {
|
|
||||||
static const std::vector<CategorySpec> kCfg = {
|
|
||||||
{"resistivity", "电阻率数据", "ERT platform inversion data", "", true, true},
|
|
||||||
{"apparent", "视电阻率数据", "visual resistivity data", "", true, true},
|
|
||||||
{"transient", "瞬变电磁数据", "DD TRANSIENT ELECTROMAGNETIC INVERSION", "", true, false},
|
|
||||||
{"voxel", "三维体", "", "dd_voxel", false, false},
|
|
||||||
// 切片不单列段——挂在三维体段「体→切片/异常」三级树下(spec §8 修订)。
|
|
||||||
};
|
|
||||||
return kCfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace geopro::app
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#include "repo/CategoryDescriptor.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
std::function<bool(const DsRow&)> byDdCode(std::initializer_list<std::string> codes) {
|
||||||
|
std::vector<std::string> cs(codes);
|
||||||
|
return [cs](const DsRow& r) {
|
||||||
|
for (const auto& c : cs) if (r.ddCode == c) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
std::function<bool(const DsRow&)> byDsTypeCode(std::initializer_list<std::string> codes) {
|
||||||
|
std::vector<std::string> cs(codes);
|
||||||
|
return [cs](const DsRow& r) {
|
||||||
|
for (const auto& c : cs) if (r.dsTypeCode == c) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<CategoryDescriptor>& categoryCatalog() {
|
||||||
|
static const std::vector<CategoryDescriptor> kCat = {
|
||||||
|
{"resistivity", "电阻率数据", SceneKind::Curtain3D,
|
||||||
|
byDsTypeCode({"ERT platform inversion data"}),
|
||||||
|
{FilterKind::DateRange, FilterKind::ArrayType},
|
||||||
|
{OpKind::GenerateVolume, OpKind::Filter}, "curtain"},
|
||||||
|
{"apparent", "视电阻率数据", SceneKind::Curtain3D,
|
||||||
|
byDsTypeCode({"visual resistivity data"}),
|
||||||
|
{FilterKind::DateRange, FilterKind::ArrayType},
|
||||||
|
{OpKind::GenerateVolume, OpKind::Filter}, "curtain"},
|
||||||
|
{"transient", "瞬变电磁数据", SceneKind::Curtain3D,
|
||||||
|
byDsTypeCode({"DD TRANSIENT ELECTROMAGNETIC INVERSION"}),
|
||||||
|
{FilterKind::DateRange},
|
||||||
|
{OpKind::GenerateVolume, OpKind::Filter}, "curtain"},
|
||||||
|
{"voxel", "三维体", SceneKind::Volume3D,
|
||||||
|
byDdCode({"dd_voxel"}),
|
||||||
|
{FilterKind::DateRange},
|
||||||
|
{OpKind::Filter}, "volume"},
|
||||||
|
{"trajectory", "轨迹数据", SceneKind::Plane2D,
|
||||||
|
byDdCode({"dd_trajectory_data"}),
|
||||||
|
{FilterKind::DateRange},
|
||||||
|
{OpKind::PlaneZ, OpKind::Filter, OpKind::Basemap}, "plane2d"},
|
||||||
|
};
|
||||||
|
return kCat;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "repo/RepoTypes.hpp" // DsRow
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
enum class SceneKind { Volume3D, Curtain3D, Plane2D }; // 渲染语义/共存规则
|
||||||
|
enum class FilterKind { DateRange, ArrayType }; // 筛选器契约(可扩展)
|
||||||
|
enum class OpKind { GenerateVolume, Filter, PlaneZ, Basemap }; // 段操作契约(可扩展)
|
||||||
|
|
||||||
|
struct CategoryDescriptor {
|
||||||
|
std::string id;
|
||||||
|
std::string title;
|
||||||
|
SceneKind sceneKind;
|
||||||
|
std::function<bool(const DsRow&)> classify; // 轴1 数据来源/分类
|
||||||
|
std::vector<FilterKind> filters; // 轴2 筛选器
|
||||||
|
std::vector<OpKind> operations; // 轴3 段头图标操作
|
||||||
|
std::string renderStrategyId; // 轴4 渲染策略键
|
||||||
|
};
|
||||||
|
|
||||||
|
// classify 便捷构造器(常见按 ddCode / dsTypeCode 接入)
|
||||||
|
std::function<bool(const DsRow&)> byDdCode(std::initializer_list<std::string> codes);
|
||||||
|
std::function<bool(const DsRow&)> byDsTypeCode(std::initializer_list<std::string> codes);
|
||||||
|
|
||||||
|
const std::vector<CategoryDescriptor>& categoryCatalog();
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
add_library(geopro_io_gpr STATIC
|
add_library(geopro_io_gpr STATIC
|
||||||
IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp
|
IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp
|
||||||
GpsTrack.cpp NormalizedRadarReader.cpp
|
GpsTrack.cpp NormalizedRadarReader.cpp LocalPath.cpp
|
||||||
RadarVolumeAssembler.cpp NormalizedRadarVolumeBridge.cpp)
|
RadarVolumeAssembler.cpp NormalizedRadarVolumeBridge.cpp)
|
||||||
target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src)
|
target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src)
|
||||||
target_compile_features(geopro_io_gpr PUBLIC cxx_std_17)
|
target_compile_features(geopro_io_gpr PUBLIC cxx_std_17)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "io/gpr/GprGeometry.hpp"
|
#include "io/gpr/GprGeometry.hpp"
|
||||||
#include "io/gpr/IprHeader.hpp"
|
#include "io/gpr/IprHeader.hpp"
|
||||||
#include "io/gpr/IprbReader.hpp"
|
#include "io/gpr/IprbReader.hpp"
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
|
|
||||||
namespace geopro::io::gpr {
|
namespace geopro::io::gpr {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -27,7 +28,7 @@ std::string toHeaderPath(const std::string& iprbPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string readFileText(const std::string& path) {
|
std::string readFileText(const std::string& path) {
|
||||||
std::ifstream f(path, std::ios::binary);
|
std::ifstream f(localPath(path), std::ios::binary);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
throw std::runtime_error("无法打开文件: " + path);
|
throw std::runtime_error("无法打开文件: " + path);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
|
|
||||||
namespace geopro::io::gpr {
|
namespace geopro::io::gpr {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -26,7 +28,7 @@ bool parseDouble(const std::string& s, double& out) {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GpsTrack parseGps(const std::string& path) {
|
GpsTrack parseGps(const std::string& path) {
|
||||||
std::ifstream f(path);
|
std::ifstream f(localPath(path));
|
||||||
if (!f) throw std::runtime_error("parseGps: 打不开 " + path);
|
if (!f) throw std::runtime_error("parseGps: 打不开 " + path);
|
||||||
|
|
||||||
GpsTrack track;
|
GpsTrack track;
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
|
|
||||||
namespace geopro::io::gpr {
|
namespace geopro::io::gpr {
|
||||||
|
|
||||||
BScan readIprb(const std::string& path, const IprHeader& h) {
|
BScan readIprb(const std::string& path, const IprHeader& h) {
|
||||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
std::ifstream f(localPath(path), std::ios::binary | std::ios::ate);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
throw std::runtime_error("readIprb: 无法打开文件: " + path);
|
throw std::runtime_error("readIprb: 无法打开文件: " + path);
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +45,7 @@ BScan readIprb(const std::string& path, const IprHeader& h) {
|
||||||
|
|
||||||
BScan readIprbRange(const std::string& path, const IprHeader& h,
|
BScan readIprbRange(const std::string& path, const IprHeader& h,
|
||||||
std::int64_t t0, std::int64_t t1) {
|
std::int64_t t0, std::int64_t t1) {
|
||||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
std::ifstream f(localPath(path), std::ios::binary | std::ios::ate);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
throw std::runtime_error("readIprbRange: 无法打开文件: " + path);
|
throw std::runtime_error("readIprbRange: 无法打开文件: " + path);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace geopro::io::gpr {
|
||||||
|
|
||||||
|
std::filesystem::path localPath(const std::string& p) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (p.empty()) return std::filesystem::path{};
|
||||||
|
const int wlen = ::MultiByteToWideChar(
|
||||||
|
CP_ACP, 0, p.data(), static_cast<int>(p.size()), nullptr, 0);
|
||||||
|
if (wlen <= 0) return std::filesystem::path(p); // 退化:原样(ASCII 安全)
|
||||||
|
std::wstring w(static_cast<std::size_t>(wlen), L'\0');
|
||||||
|
::MultiByteToWideChar(CP_ACP, 0, p.data(), static_cast<int>(p.size()),
|
||||||
|
w.data(), wlen);
|
||||||
|
return std::filesystem::path(w);
|
||||||
|
#else
|
||||||
|
return std::filesystem::path(p); // POSIX:窄字节即 UTF-8 原生路径
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::io::gpr
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace geopro::io::gpr {
|
||||||
|
|
||||||
|
// 把"本地 8 位编码"的窄字节路径转成 std::filesystem::path。
|
||||||
|
// Windows:源串按当前 ANSI 代码页(简中=GBK/936,即 QString::toLocal8Bit 产物)
|
||||||
|
// 解码为宽字符 path,使 std::ifstream/ofstream 走宽字符打开 —— 否则
|
||||||
|
// 窄字符 ifstream 在默认 "C" locale 下无法解析含中文的路径,open 失败。
|
||||||
|
// 其它平台:窄字节即原生 UTF-8 路径,直接构造。
|
||||||
|
// 退化保护:转换失败时原样返回(ASCII 路径不受影响)。
|
||||||
|
std::filesystem::path localPath(const std::string& p);
|
||||||
|
|
||||||
|
} // namespace geopro::io::gpr
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "io/gpr/NormalizedRadarReader.hpp"
|
#include "io/gpr/NormalizedRadarReader.hpp"
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
@ -76,11 +77,11 @@ std::vector<std::int16_t> readRadarDataCube(const std::string& dataPath,
|
||||||
const std::size_t n = static_cast<std::size_t>(h.lastTrace) * h.samples;
|
const std::size_t n = static_cast<std::size_t>(h.lastTrace) * h.samples;
|
||||||
const std::uintmax_t expect = static_cast<std::uintmax_t>(n) * 2;
|
const std::uintmax_t expect = static_cast<std::uintmax_t>(n) * 2;
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
const auto fsize = std::filesystem::file_size(dataPath, ec);
|
const auto fsize = std::filesystem::file_size(localPath(dataPath), ec);
|
||||||
if (ec || fsize != expect)
|
if (ec || fsize != expect)
|
||||||
throw std::runtime_error("规范化 .data 大小不符: " + dataPath);
|
throw std::runtime_error("规范化 .data 大小不符: " + dataPath);
|
||||||
std::vector<std::int16_t> cube(n);
|
std::vector<std::int16_t> cube(n);
|
||||||
std::ifstream f(dataPath, std::ios::binary);
|
std::ifstream f(localPath(dataPath), std::ios::binary);
|
||||||
if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath);
|
if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath);
|
||||||
f.read(reinterpret_cast<char*>(cube.data()), static_cast<std::streamsize>(expect));
|
f.read(reinterpret_cast<char*>(cube.data()), static_cast<std::streamsize>(expect));
|
||||||
if (!f) throw std::runtime_error("读 .data 失败: " + dataPath);
|
if (!f) throw std::runtime_error("读 .data 失败: " + dataPath);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "io/gpr/LocalPath.hpp"
|
||||||
#include "io/gpr/NormalizedRadarReader.hpp"
|
#include "io/gpr/NormalizedRadarReader.hpp"
|
||||||
#include "io/gpr/RadarVolumeAssembler.hpp"
|
#include "io/gpr/RadarVolumeAssembler.hpp"
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir,
|
||||||
|
|
||||||
std::string headText;
|
std::string headText;
|
||||||
{
|
{
|
||||||
std::ifstream f(head);
|
std::ifstream f(localPath(head));
|
||||||
if (!f) throw std::runtime_error("打开 .head 失败: " + head);
|
if (!f) throw std::runtime_error("打开 .head 失败: " + head);
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << f.rdbuf();
|
ss << f.rdbuf();
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,29 @@ void applyView(vtkRenderer* r, ViewDir dir)
|
||||||
r->ResetCamera();
|
r->ResetCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CameraPose orbitPose(ViewDir dir, const double pivot[3], double distance)
|
||||||
|
{
|
||||||
|
// 方向偏移(pos = pivot + offset*distance)与 up 约定须与 applyView 完全一致:
|
||||||
|
// Top +Z/up+Y、Bottom -Z/up+Y、Front -Y/up+Z、Back +Y/up+Z、Left -X/up+Z、Right +X/up+Z。
|
||||||
|
double off[3] = {0, 0, 0};
|
||||||
|
double up[3] = {0, 0, 1};
|
||||||
|
switch (dir) {
|
||||||
|
case ViewDir::Top: off[2] = 1; up[0] = 0; up[1] = 1; up[2] = 0; break;
|
||||||
|
case ViewDir::Bottom: off[2] = -1; up[0] = 0; up[1] = 1; up[2] = 0; break;
|
||||||
|
case ViewDir::Front: off[1] = -1; break; // 从 -Y 看 +Y,up=+Z
|
||||||
|
case ViewDir::Back: off[1] = 1; break;
|
||||||
|
case ViewDir::Left: off[0] = -1; break; // 从 -X 看 +X,up=+Z
|
||||||
|
case ViewDir::Right: off[0] = 1; break;
|
||||||
|
}
|
||||||
|
CameraPose pose;
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
pose.focal[i] = pivot[i];
|
||||||
|
pose.pos[i] = pivot[i] + off[i] * distance;
|
||||||
|
pose.up[i] = up[i];
|
||||||
|
}
|
||||||
|
return pose;
|
||||||
|
}
|
||||||
|
|
||||||
void zoomBy(vtkRenderer* r, double factor)
|
void zoomBy(vtkRenderer* r, double factor)
|
||||||
{
|
{
|
||||||
if (!r || factor <= 0.0) return;
|
if (!r || factor <= 0.0) return;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,16 @@ enum class ViewDir { Front, Back, Left, Right, Top, Bottom };
|
||||||
// 应用 6 向正交快捷视图:设 position/focalPoint/viewUp 后 ResetCamera。
|
// 应用 6 向正交快捷视图:设 position/focalPoint/viewUp 后 ResetCamera。
|
||||||
void applyView(vtkRenderer* r, ViewDir dir);
|
void applyView(vtkRenderer* r, ViewDir dir);
|
||||||
|
|
||||||
|
// 绕支点转到某轴的相机位姿(纯数学,可单测):focal=pivot,pos=pivot+dir_offset*distance,
|
||||||
|
// up 按 dir 预设。方向偏移/up 约定与 applyView 完全一致(Top=+Z 看下、+Y 朝上;Front 从 -Y
|
||||||
|
// 看 +Y、+Z 朝上;…)。用于 orbitToAxis:保留当前缩放距离、只改朝向绕 pivot 转。
|
||||||
|
struct CameraPose {
|
||||||
|
double pos[3];
|
||||||
|
double focal[3];
|
||||||
|
double up[3];
|
||||||
|
};
|
||||||
|
CameraPose orbitPose(ViewDir dir, const double pivot[3], double distance);
|
||||||
|
|
||||||
// 相机缩放:factor>1 拉近(放大),factor<1 推远(缩小)。透视下改距离、正交下改 parallelScale。
|
// 相机缩放:factor>1 拉近(放大),factor<1 推远(缩小)。透视下改距离、正交下改 parallelScale。
|
||||||
void zoomBy(vtkRenderer* r, double factor);
|
void zoomBy(vtkRenderer* r, double factor);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,27 +293,6 @@ void InteractionManager::closeAll() {
|
||||||
safeRender();
|
safeRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
PickInteractorStyle* InteractionManager::pickStyle() const { return style_; }
|
|
||||||
|
|
||||||
void InteractionManager::setMode2D(bool is2D) {
|
|
||||||
// 进入二维分析:主动取消「三维前视图」的所有选中。否则残留的选中切片会让 onWheel 持续消费滚轮
|
|
||||||
// (二维下无法缩放),且切回三维仍残留高亮。清 selected_ + 切片高亮;再经 onSliceSelectionChanged("")
|
|
||||||
// 联动清三维分析列表选中行与异常高亮(app 层接线)。与 VtkSceneView::setAnalysisMode2D 离开二维时
|
|
||||||
// clearMapLineSelection 清足迹选中相对称。
|
|
||||||
if (is2D) {
|
|
||||||
if (selected_ >= 0) {
|
|
||||||
selected_ = -1;
|
|
||||||
updateSelectionVisual(); // 清切片高亮(切回三维不残留选中)
|
|
||||||
}
|
|
||||||
if (onSliceSelectionChanged) onSliceSelectionChanged(std::string{});
|
|
||||||
}
|
|
||||||
// 切片属三维内容:二维分析隐藏(不销毁→切回零重建)、三维分析显示。
|
|
||||||
for (auto& s : slices_)
|
|
||||||
if (s) s->setVisible(!is2D);
|
|
||||||
if (style_) style_->setLock2D(is2D); // 二维=禁旋转、左键平移(仅平移+缩放)
|
|
||||||
// 不在此渲染:相机/地形/底图/维度显隐及统一 Render 由 VtkSceneView::setAnalysisMode2D 收尾。
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractionManager::flipView() {
|
void InteractionManager::flipView() {
|
||||||
if (!renderer_) return;
|
if (!renderer_) return;
|
||||||
auto* cam = renderer_->GetActiveCamera();
|
auto* cam = renderer_->GetActiveCamera();
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,6 @@ public:
|
||||||
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
|
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
|
||||||
void closeAll();
|
void closeAll();
|
||||||
|
|
||||||
// 切二维分析(is2D=true)/三维分析(false):翻所有切片显隐(不销毁,切回零重建) + 锁/解锁交互样式
|
|
||||||
// (二维=仅平移+缩放、禁旋转)。地形/底图/相机由 VtkSceneView::setAnalysisMode2D 处理,此处不渲染。
|
|
||||||
void setMode2D(bool is2D);
|
|
||||||
// 关闭并释放某体下的所有切片(该体移除/重建时;不动其它体的切片)。
|
// 关闭并释放某体下的所有切片(该体移除/重建时;不动其它体的切片)。
|
||||||
void closeSlicesOfVolume(const std::string& volumeDsId);
|
void closeSlicesOfVolume(const std::string& volumeDsId);
|
||||||
|
|
||||||
|
|
@ -126,10 +123,6 @@ public:
|
||||||
void installStyle();
|
void installStyle();
|
||||||
void uninstallStyle();
|
void uninstallStyle();
|
||||||
|
|
||||||
// 暴露交互样式:供 app 层注入二维分析 B 期的足迹拾取/Z 拖动回调(onPick2D/onDrag2D/onDrag2DEnd)。
|
|
||||||
// 定义在 .cpp(此处 PickInteractorStyle 仅前置声明,vtkSmartPointer→裸指针下转需完整类型)。
|
|
||||||
PickInteractorStyle* pickStyle() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// 拾取回调实现(PickInteractorStyle 注入)。
|
// 拾取回调实现(PickInteractorStyle 注入)。
|
||||||
void onPicked(const Vec3& worldPoint); // 选中所在切片 + 焦点
|
void onPicked(const Vec3& worldPoint); // 选中所在切片 + 焦点
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#include "interact/PickInteractorStyle.hpp"
|
#include "interact/PickInteractorStyle.hpp"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include <vtkCallbackCommand.h>
|
#include <vtkCallbackCommand.h>
|
||||||
|
|
@ -50,22 +49,6 @@ bool PickInteractorStyle::pickWorld(Vec3& out) {
|
||||||
|
|
||||||
void PickInteractorStyle::OnLeftButtonDown() {
|
void PickInteractorStyle::OnLeftButtonDown() {
|
||||||
auto* iren = this->GetInteractor();
|
auto* iren = this->GetInteractor();
|
||||||
// 二维分析:左键命中足迹→进入高程 Z 拖动(B 期);否则=平移(等同中键),禁旋转。抬键由 OnLeftButtonUp 收尾。
|
|
||||||
if (lock2D_) {
|
|
||||||
const int* p = iren ? iren->GetEventPosition() : nullptr;
|
|
||||||
if (p) this->FindPokedRenderer(p[0], p[1]);
|
|
||||||
if (!this->CurrentRenderer) return;
|
|
||||||
const bool additive = iren && iren->GetControlKey(); // Ctrl=多选
|
|
||||||
if (onPick2D && p && onPick2D(p[0], p[1], additive)) { // 命中足迹 → Z 拖动
|
|
||||||
dragging2D_ = true;
|
|
||||||
lastDragY_ = p[1];
|
|
||||||
this->GrabFocus(this->EventCallbackCommand);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->GrabFocus(this->EventCallbackCommand); // 未命中 → 平移
|
|
||||||
this->StartPan();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vec3 world;
|
Vec3 world;
|
||||||
const bool hit = pickWorld(world); // 仍用于取选中所需世界点(onPick)
|
const bool hit = pickWorld(world); // 仍用于取选中所需世界点(onPick)
|
||||||
// 命中切片【精确判定】:光标射线穿过某切片真实矩形内才算(不靠带容差的 picker 点)。
|
// 命中切片【精确判定】:光标射线穿过某切片真实矩形内才算(不靠带容差的 picker 点)。
|
||||||
|
|
@ -107,7 +90,6 @@ void PickInteractorStyle::OnLeftButtonDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void PickInteractorStyle::Rotate() {
|
void PickInteractorStyle::Rotate() {
|
||||||
if (lock2D_) return; // 二维分析禁旋转(仅平移+缩放)
|
|
||||||
if (!this->CurrentRenderer || !hasRotatePivot_) {
|
if (!this->CurrentRenderer || !hasRotatePivot_) {
|
||||||
Superclass::Rotate(); // 无支点 → 默认绕焦点旋转
|
Superclass::Rotate(); // 无支点 → 默认绕焦点旋转
|
||||||
return;
|
return;
|
||||||
|
|
@ -154,49 +136,14 @@ void PickInteractorStyle::Rotate() {
|
||||||
rwi->Render();
|
rwi->Render();
|
||||||
}
|
}
|
||||||
|
|
||||||
double PickInteractorStyle::worldPerPixelZ() const {
|
|
||||||
if (!this->CurrentRenderer) return 1.0;
|
|
||||||
auto* cam = this->CurrentRenderer->GetActiveCamera();
|
|
||||||
auto* rw = this->CurrentRenderer->GetRenderWindow();
|
|
||||||
if (!cam || !rw) return 1.0;
|
|
||||||
const int* sz = rw->GetSize();
|
|
||||||
const double h = (sz && sz[1] > 0) ? static_cast<double>(sz[1]) : 800.0;
|
|
||||||
if (cam->GetParallelProjection())
|
|
||||||
return 2.0 * cam->GetParallelScale() / h; // 平行投影:可见世界高度=2*parallelScale
|
|
||||||
// 透视:可见世界高度 = 2*d*tan(viewAngle/2),d=相机到焦点距离。
|
|
||||||
double pos[3], fp[3];
|
|
||||||
cam->GetPosition(pos);
|
|
||||||
cam->GetFocalPoint(fp);
|
|
||||||
const double dx = pos[0] - fp[0], dy = pos[1] - fp[1], dz = pos[2] - fp[2];
|
|
||||||
const double d = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
const double va = vtkMath::RadiansFromDegrees(cam->GetViewAngle());
|
|
||||||
return 2.0 * d * std::tan(va * 0.5) / h;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PickInteractorStyle::OnMouseMove() {
|
void PickInteractorStyle::OnMouseMove() {
|
||||||
if (dragging2D_) { // B 期:竖向拖动 → 选中足迹 Z 增量(仅改 Z)。鼠标上移(y 增)→ 抬高。
|
|
||||||
auto* rwi = this->Interactor;
|
|
||||||
if (rwi) {
|
|
||||||
const int y = rwi->GetEventPosition()[1];
|
|
||||||
const int dyPix = y - lastDragY_;
|
|
||||||
lastDragY_ = y;
|
|
||||||
if (dyPix != 0 && onDrag2D) onDrag2D(worldPerPixelZ() * dyPix);
|
|
||||||
}
|
|
||||||
return; // 不走基类(不平移/不旋转)
|
|
||||||
}
|
|
||||||
Superclass::OnMouseMove();
|
Superclass::OnMouseMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PickInteractorStyle::OnLeftButtonUp() {
|
void PickInteractorStyle::OnLeftButtonUp() {
|
||||||
if (dragging2D_) { // 结束 Z 拖动
|
|
||||||
dragging2D_ = false;
|
|
||||||
if (this->Interactor) this->ReleaseFocus();
|
|
||||||
if (onDrag2DEnd) onDrag2DEnd();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 单击(抬键位移<阈值=非拖动)且按下未命中切片 → 取消选中(点空/点体;体 PickableOff 故点体也 hit=false)。
|
// 单击(抬键位移<阈值=非拖动)且按下未命中切片 → 取消选中(点空/点体;体 PickableOff 故点体也 hit=false)。
|
||||||
// 拖空白旋转:抬键位移大 → 不取消,保留"绕选中切片旋转"。Esc 仍是完全拉近时的兜底。
|
// 拖空白旋转:抬键位移大 → 不取消,保留"绕选中切片旋转"。Esc 仍是完全拉近时的兜底。
|
||||||
if (!lock2D_ && !downHitSlice_ && onDeselect) {
|
if (!downHitSlice_ && onDeselect) {
|
||||||
auto* iren = this->GetInteractor();
|
auto* iren = this->GetInteractor();
|
||||||
const int* up = iren ? iren->GetEventPosition() : nullptr;
|
const int* up = iren ? iren->GetEventPosition() : nullptr;
|
||||||
if (up) {
|
if (up) {
|
||||||
|
|
@ -208,19 +155,13 @@ void PickInteractorStyle::OnLeftButtonUp() {
|
||||||
Superclass::OnLeftButtonUp(); // 平移/旋转/缩放等由基类按 State 收尾
|
Superclass::OnLeftButtonUp(); // 平移/旋转/缩放等由基类按 State 收尾
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr double kWheelStepPx = 24.0; // 滚轮一格升降 ≈ 拖动 24 像素的世界 Z 量(与拖动手感一致)
|
|
||||||
}
|
|
||||||
|
|
||||||
void PickInteractorStyle::OnMouseWheelForward() {
|
void PickInteractorStyle::OnMouseWheelForward() {
|
||||||
// 二维分析有选中足迹 → 滚轮抬升其高程(消费滚轮);否则按切片推进 / 默认缩放。
|
// 有选中切片 → 沿法向推进(消费滚轮);否则默认缩放。
|
||||||
if (lock2D_ && onWheel2D && onWheel2D(worldPerPixelZ() * kWheelStepPx)) return;
|
|
||||||
if (onWheelStep && onWheelStep(+1)) return; // 有选中切片 → 推进,消费滚轮
|
if (onWheelStep && onWheelStep(+1)) return; // 有选中切片 → 推进,消费滚轮
|
||||||
Superclass::OnMouseWheelForward(); // 否则默认缩放
|
Superclass::OnMouseWheelForward(); // 否则默认缩放
|
||||||
}
|
}
|
||||||
|
|
||||||
void PickInteractorStyle::OnMouseWheelBackward() {
|
void PickInteractorStyle::OnMouseWheelBackward() {
|
||||||
if (lock2D_ && onWheel2D && onWheel2D(-worldPerPixelZ() * kWheelStepPx)) return;
|
|
||||||
if (onWheelStep && onWheelStep(-1)) return;
|
if (onWheelStep && onWheelStep(-1)) return;
|
||||||
Superclass::OnMouseWheelBackward();
|
Superclass::OnMouseWheelBackward();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,20 +37,6 @@ public:
|
||||||
// 点帘面/其它非切片物/边界外 → 返回 false → 单击即取消选中。
|
// 点帘面/其它非切片物/边界外 → 返回 false → 单击即取消选中。
|
||||||
std::function<bool()> hitTestSlice;
|
std::function<bool()> hitTestSlice;
|
||||||
|
|
||||||
// 二维分析锁:开 → 左键拖动改为平移、禁旋转(仅平移+缩放);关 → 恢复三维拾取/旋转交互。
|
|
||||||
void setLock2D(bool on) { lock2D_ = on; }
|
|
||||||
bool isLock2D() const { return lock2D_; }
|
|
||||||
|
|
||||||
// ── 二维分析 B 期:选中足迹沿高程 Z 拖动 ──(仅 lock2D 下生效;回调由 app 层注入)
|
|
||||||
// onPick2D:左键按下时在(x,y)拾取足迹(additive=Ctrl 多选),返回是否有选中→有则进入 Z 拖动、否则平移。
|
|
||||||
// onDrag2D:拖动中把竖向像素换算成的世界 Z 增量(本类按相机算)交给 app 施加到选中足迹(仅改 Z)。
|
|
||||||
// onDrag2DEnd:松开结束拖动(供 app 收起高程读数浮层)。
|
|
||||||
std::function<bool(int x, int y, bool additive)> onPick2D;
|
|
||||||
std::function<void(double worldDz)> onDrag2D;
|
|
||||||
std::function<void()> onDrag2DEnd;
|
|
||||||
// 滚轮升降:有选中足迹时滚轮改其高程 Z(本类按相机算 worldDz);app 施加并返回是否消费(无选中→false→默认缩放)。
|
|
||||||
std::function<bool(double worldDz)> onWheel2D;
|
|
||||||
|
|
||||||
void OnMouseMove() override;
|
void OnMouseMove() override;
|
||||||
void OnLeftButtonUp() override;
|
void OnLeftButtonUp() override;
|
||||||
|
|
||||||
|
|
@ -68,8 +54,6 @@ protected:
|
||||||
private:
|
private:
|
||||||
// 在当前鼠标位置拾取世界点;命中返回 true 并填 out。
|
// 在当前鼠标位置拾取世界点;命中返回 true 并填 out。
|
||||||
bool pickWorld(Vec3& out);
|
bool pickWorld(Vec3& out);
|
||||||
// 当前相机下:竖向一屏幕像素对应的世界 Z(米/像素),用于把拖动像素换算成 Z 增量。
|
|
||||||
double worldPerPixelZ() const;
|
|
||||||
|
|
||||||
// 手动双击判定:QVTK+Windows 下 vtkRenderWindowInteractor::GetRepeatCount() 不可靠(评审 M5)。
|
// 手动双击判定:QVTK+Windows 下 vtkRenderWindowInteractor::GetRepeatCount() 不可靠(评审 M5)。
|
||||||
// 记上次左键按下时刻+屏幕位置,两次按下间隔 < kDoubleClickMs 且位置相近视为双击。
|
// 记上次左键按下时刻+屏幕位置,两次按下间隔 < kDoubleClickMs 且位置相近视为双击。
|
||||||
|
|
@ -84,13 +68,6 @@ private:
|
||||||
// 选中切片=其中心;否则=光标射线穿过的体中段点。无则 hasRotatePivot_=false→默认绕焦点。
|
// 选中切片=其中心;否则=光标射线穿过的体中段点。无则 hasRotatePivot_=false→默认绕焦点。
|
||||||
Vec3 rotatePivot_{};
|
Vec3 rotatePivot_{};
|
||||||
bool hasRotatePivot_ = false;
|
bool hasRotatePivot_ = false;
|
||||||
|
|
||||||
// 二维分析模式:左键=平移、禁旋转(仅平移+缩放)。由 InteractionManager 在切 tab 时设。
|
|
||||||
bool lock2D_ = false;
|
|
||||||
|
|
||||||
// B 期足迹 Z 拖动状态:左键命中足迹时进入,记上次鼠标 y 以算增量。
|
|
||||||
bool dragging2D_ = false;
|
|
||||||
int lastDragY_ = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::render::interact
|
} // namespace geopro::render::interact
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ target_sources(geopro_tests PRIVATE data/test_nav_request.cpp)
|
||||||
# GprVolumeRepository:逐线 GPR int16 量化体(BuiltI16)→ app 渲染链 float 体(VolumeGrid)。
|
# GprVolumeRepository:逐线 GPR int16 量化体(BuiltI16)→ app 渲染链 float 体(VolumeGrid)。
|
||||||
# 纯适配器逐值反量化 + 全链(合成多通道 .iprb 走真 P1/P2)产出有效 VolumeGrid。
|
# 纯适配器逐值反量化 + 全链(合成多通道 .iprb 走真 P1/P2)产出有效 VolumeGrid。
|
||||||
target_sources(geopro_tests PRIVATE data/test_gpr_volume_repository.cpp)
|
target_sources(geopro_tests PRIVATE data/test_gpr_volume_repository.cpp)
|
||||||
|
# CategoryDescriptor:类目描述符目录 categoryCatalog(classify谓词+扩展契约) + splitByCategory 遍历路由。
|
||||||
|
target_sources(geopro_tests PRIVATE data/test_category_descriptor.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||||
|
|
||||||
# store 层:ChunkedVolumeStore(GPR 三维体分块压缩落盘 round-trip + 边缘块 + 压缩生效)。
|
# store 层:ChunkedVolumeStore(GPR 三维体分块压缩落盘 round-trip + 边缘块 + 压缩生效)。
|
||||||
|
|
@ -170,11 +172,6 @@ target_sources(geopro_tests PRIVATE
|
||||||
app/test_color_scale_io.cpp
|
app/test_color_scale_io.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/app/ColorScaleIO.cpp
|
${CMAKE_SOURCE_DIR}/src/app/ColorScaleIO.cpp
|
||||||
)
|
)
|
||||||
# 维度过滤纯函数(splitByDimension: ddCode -> 三维/二维/分析三栏,无 Qt/VTK 依赖)。
|
|
||||||
target_sources(geopro_tests PRIVATE
|
|
||||||
app/test_dataset_dimension.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/src/app/DatasetDimension.cpp
|
|
||||||
)
|
|
||||||
# 大类分类纯函数(splitByCategory: dsTypeCode/ddCode -> 5 个数据类型大类段,无 Qt/VTK 依赖)。
|
# 大类分类纯函数(splitByCategory: dsTypeCode/ddCode -> 5 个数据类型大类段,无 Qt/VTK 依赖)。
|
||||||
target_sources(geopro_tests PRIVATE
|
target_sources(geopro_tests PRIVATE
|
||||||
app/test_dataset_category.cpp
|
app/test_dataset_category.cpp
|
||||||
|
|
@ -182,6 +179,16 @@ target_sources(geopro_tests PRIVATE
|
||||||
)
|
)
|
||||||
# 对象树勾选纯逻辑(GS 三态聚合 aggregateGsState + 勾选源去重 dedupeSources,header-only)。
|
# 对象树勾选纯逻辑(GS 三态聚合 aggregateGsState + 勾选源去重 dedupeSources,header-only)。
|
||||||
target_sources(geopro_tests PRIVATE app/test_object_tree_selection.cpp)
|
target_sources(geopro_tests PRIVATE app/test_object_tree_selection.cpp)
|
||||||
|
# 段头响应式图标工具条溢出计算纯逻辑(visibleIconCount,见 spec §6)。SectionIconBar.cpp 还含
|
||||||
|
# QWidget 组件,其按钮构造依赖 makeGlyph(Glyphs.cpp) 与主题(Theme.cpp),故一并加入并链 Qt6::Widgets/Svg。
|
||||||
|
find_package(Qt6 COMPONENTS Widgets Svg REQUIRED)
|
||||||
|
target_sources(geopro_tests PRIVATE
|
||||||
|
app/test_section_icon_bar.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/panels/columns/SectionIconBar.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/Glyphs.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/Theme.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(geopro_tests PRIVATE Qt6::Widgets Qt6::Svg)
|
||||||
# measurement 散点纯逻辑(值类型变换 / 显隐 id 收集 / 过滤体 / 另存体,Qt6::Core JSON + core model)。
|
# measurement 散点纯逻辑(值类型变换 / 显隐 id 收集 / 过滤体 / 另存体,Qt6::Core JSON + core model)。
|
||||||
target_sources(geopro_tests PRIVATE
|
target_sources(geopro_tests PRIVATE
|
||||||
app/test_scatter_data_ops.cpp
|
app/test_scatter_data_ops.cpp
|
||||||
|
|
@ -216,6 +223,10 @@ target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cp
|
||||||
target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp)
|
target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp)
|
||||||
# VtkSceneController 编排:注入 fake repo + fake view,断言 视图模式×图层 组合下 add 的图元类型/数量;取消勾选清空。
|
# VtkSceneController 编排:注入 fake repo + fake view,断言 视图模式×图层 组合下 add 的图元类型/数量;取消勾选清空。
|
||||||
target_sources(geopro_tests PRIVATE controller/test_vtk_scene_controller.cpp)
|
target_sources(geopro_tests PRIVATE controller/test_vtk_scene_controller.cpp)
|
||||||
|
# RenderStrategyRegistry:字符串键策略注册表解析(register/get,纯逻辑,FakeStrategy)。
|
||||||
|
target_sources(geopro_tests PRIVATE controller/test_render_strategy_registry.cpp)
|
||||||
|
# PlaneZRegistry:按 2D 类型管理平面 z 生命周期(首勾定 z / 全消消失 / setPlaneZ 整体调,纯逻辑头)。
|
||||||
|
target_sources(geopro_tests PRIVATE controller/test_plane_z_registry.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test)
|
target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test)
|
||||||
|
|
||||||
# io/gpr 层:.iprh 头解析 + .iprb B-scan 读取(纯 C++17,零 Qt/VTK)。
|
# io/gpr 层:.iprh 头解析 + .iprb B-scan 读取(纯 C++17,零 Qt/VTK)。
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include "DatasetDetailTab.hpp"
|
#include "DatasetDetailTab.hpp"
|
||||||
#include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层)
|
#include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层)
|
||||||
|
#include "panels/chart/ErtInversionStrategy.hpp"
|
||||||
#include "panels/chart/MeasurementStrategy.hpp"
|
#include "panels/chart/MeasurementStrategy.hpp"
|
||||||
#include "panels/chart/GrMeasurementStrategy.hpp"
|
#include "panels/chart/GrMeasurementStrategy.hpp"
|
||||||
#include "panels/chart/TrajectoryStrategy.hpp"
|
#include "panels/chart/TrajectoryStrategy.hpp"
|
||||||
|
|
@ -39,6 +40,29 @@ TEST(ChartStrategyRegistry, ExposesTabSpecsFromStrategy) {
|
||||||
EXPECT_EQ(tabs[1].kind, ViewKind::FilledContour);
|
EXPECT_EQ(tabs[1].kind, ViewKind::FilledContour);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T4 双击详情联动 gate 契约(决策6):DatasetDetailController::supports() 直接委托本注册表的 supports()。
|
||||||
|
// 三维体(dd_voxel/dd_radar_3d)等无详情页策略 → supports()=false → 双击只适配、静默不开面板;
|
||||||
|
// 5 种已注册类型 supports()=true → 双击联动打开中下方详情页。此测锁定这条 gate 的真实注册集行为。
|
||||||
|
TEST(ChartStrategyRegistry, T4GateSilentForVolumeTypesSupportsRegistered) {
|
||||||
|
ChartStrategyRegistry reg;
|
||||||
|
reg.add(std::make_unique<geopro::app::ErtInversionStrategy>());
|
||||||
|
reg.add(std::make_unique<geopro::app::MeasurementStrategy>());
|
||||||
|
reg.add(std::make_unique<geopro::app::GrMeasurementStrategy>());
|
||||||
|
reg.add(std::make_unique<geopro::app::TrajectoryStrategy>());
|
||||||
|
reg.add(std::make_unique<geopro::app::GridStrategy>());
|
||||||
|
// 有详情页 → 双击联动打开中下方详情面板。
|
||||||
|
EXPECT_TRUE(reg.supports("dd_inversion_data"));
|
||||||
|
EXPECT_TRUE(reg.supports("dd_ert_measurement_data"));
|
||||||
|
EXPECT_TRUE(reg.supports("dd_ert_measurement_gr_data"));
|
||||||
|
EXPECT_TRUE(reg.supports("dd_trajectory_data"));
|
||||||
|
EXPECT_TRUE(reg.supports("dd_grid"));
|
||||||
|
// 无详情页(三维体/切片/异常)→ 静默:双击只适配、不开面板、不弹状态栏。
|
||||||
|
EXPECT_FALSE(reg.supports("dd_voxel"));
|
||||||
|
EXPECT_FALSE(reg.supports("dd_radar_3d"));
|
||||||
|
EXPECT_FALSE(reg.supports("dd_slice"));
|
||||||
|
EXPECT_FALSE(reg.supports("dd_anomaly"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MeasurementStrategy, DrivesTwoTabsScatterAndTable) {
|
TEST(MeasurementStrategy, DrivesTwoTabsScatterAndTable) {
|
||||||
geopro::app::MeasurementStrategy s;
|
geopro::app::MeasurementStrategy s;
|
||||||
EXPECT_EQ(s.ddCode(), "dd_ert_measurement_data");
|
EXPECT_EQ(s.ddCode(), "dd_ert_measurement_data");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include "DatasetCategory.hpp"
|
#include "DatasetCategory.hpp"
|
||||||
|
#include "repo/CategoryDescriptor.hpp"
|
||||||
using geopro::data::DsRow;
|
using geopro::data::DsRow;
|
||||||
using namespace geopro::app;
|
using namespace geopro::app;
|
||||||
|
|
||||||
|
|
@ -23,7 +24,8 @@ TEST(SplitByCategory, RoutesByDsTypeCodeAndDdCode) {
|
||||||
row("x", "dd_ert_measurement_gr_data", "earth resistance"), // 接地电阻 → 丢弃
|
row("x", "dd_ert_measurement_gr_data", "earth resistance"), // 接地电阻 → 丢弃
|
||||||
};
|
};
|
||||||
const CategoryBuckets b = splitByCategory(rows);
|
const CategoryBuckets b = splitByCategory(rows);
|
||||||
ASSERT_EQ(b.segments.size(), categoryConfigs().size());
|
// splitByCategory 现走 categoryCatalog()(5 段,含 trajectory);旧 categoryConfigs 暂保留供 UI。
|
||||||
|
ASSERT_EQ(b.segments.size(), geopro::data::categoryCatalog().size());
|
||||||
EXPECT_EQ(b.segments[0].size(), 1u); EXPECT_EQ(b.segments[0][0].id, "a");
|
EXPECT_EQ(b.segments[0].size(), 1u); EXPECT_EQ(b.segments[0][0].id, "a");
|
||||||
EXPECT_EQ(b.segments[1].size(), 1u); EXPECT_EQ(b.segments[1][0].id, "b");
|
EXPECT_EQ(b.segments[1].size(), 1u); EXPECT_EQ(b.segments[1][0].id, "b");
|
||||||
EXPECT_EQ(b.segments[2].size(), 1u); EXPECT_EQ(b.segments[2][0].id, "c");
|
EXPECT_EQ(b.segments[2].size(), 1u); EXPECT_EQ(b.segments[2][0].id, "c");
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
#include <gtest/gtest.h>
|
|
||||||
#include "DatasetDimension.hpp"
|
|
||||||
#include "repo/RepoTypes.hpp"
|
|
||||||
|
|
||||||
using geopro::data::DsRow;
|
|
||||||
using geopro::app::splitByDimension;
|
|
||||||
using geopro::app::DimBuckets;
|
|
||||||
|
|
||||||
static DsRow row(const char* id, const char* ddCode) {
|
|
||||||
DsRow r; r.id = id; r.ddCode = ddCode; return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DatasetDimension, SplitsByDdCode) {
|
|
||||||
std::vector<DsRow> in{
|
|
||||||
row("a", "dd_section"), // 3D
|
|
||||||
row("b", "dd_voxel"), // 3D
|
|
||||||
row("f", "dd_radar_3d"), // 3D(三维雷达体,spec §6.1)
|
|
||||||
row("c", "dd_trajectory_data"), // 2D
|
|
||||||
row("d", "dd_slice"), // Analysis
|
|
||||||
row("e", "dd_unknownxyz"), // Other -> not in any bucket
|
|
||||||
};
|
|
||||||
DimBuckets b = splitByDimension(in);
|
|
||||||
ASSERT_EQ(b.dim3D.size(), 3u);
|
|
||||||
EXPECT_EQ(b.dim3D[0].id, "a");
|
|
||||||
EXPECT_EQ(b.dim3D[1].id, "b");
|
|
||||||
EXPECT_EQ(b.dim3D[2].id, "f");
|
|
||||||
ASSERT_EQ(b.dim2D.size(), 1u);
|
|
||||||
EXPECT_EQ(b.dim2D[0].id, "c");
|
|
||||||
ASSERT_EQ(b.analysis.size(), 1u);
|
|
||||||
EXPECT_EQ(b.analysis[0].id, "d");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DatasetDimension, EmptyInput) {
|
|
||||||
DimBuckets b = splitByDimension({});
|
|
||||||
EXPECT_TRUE(b.dim3D.empty());
|
|
||||||
EXPECT_TRUE(b.dim2D.empty());
|
|
||||||
EXPECT_TRUE(b.analysis.empty());
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
|
|
||||||
|
using geopro::app::visibleIconCount;
|
||||||
|
|
||||||
|
TEST(SectionIconBar, ShowsAllWhenWideEnoughAndUnderMax) {
|
||||||
|
// 3 图标, 宽 1000, 每个 30, 溢出 30, max 3 → 全显
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 1000, 30, 30, 3), 3);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, CapsAtMaxIcons) {
|
||||||
|
// 5 图标但 max 3, 宽足够 → 显 3, 其余 2 进溢出(此时需留溢出位)
|
||||||
|
EXPECT_EQ(visibleIconCount(5, 1000, 30, 30, 3), 3);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, FoldsRightWhenNarrow) {
|
||||||
|
// 3 图标, max 3, 但宽只够 2 个 + 溢出: 75px, 30 each, overflow 30 → 2*30+30=90>75 → 1*30+30=60<=75 → 1
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 75, 30, 30, 3), 1);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, NoOverflowReserveWhenAllFit) {
|
||||||
|
// 2 图标全显且 <=max, 不需溢出位: 宽 60 恰好 2*30
|
||||||
|
EXPECT_EQ(visibleIconCount(2, 60, 30, 30, 3), 2);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, ZeroWhenTooNarrow) {
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 20, 30, 30, 3), 0);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "controller/PlaneZRegistry.hpp"
|
||||||
|
|
||||||
|
using geopro::controller::PlaneZRegistry;
|
||||||
|
|
||||||
|
TEST(PlaneZRegistry, FirstCheckSetsPlaneZ) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "a", 12.0), 12.0);
|
||||||
|
EXPECT_TRUE(r.hasPlane("trajectory"));
|
||||||
|
EXPECT_DOUBLE_EQ(r.planeZ("trajectory"), 12.0);
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, SecondCheckKeepsFirstZ) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "b", 99.0), 12.0); // 投影到首个 ds 的平面
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, PlaneDisappearsWhenAllUnchecked) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
r.onChecked("trajectory", "b", 99.0);
|
||||||
|
r.onUnchecked("trajectory", "a");
|
||||||
|
EXPECT_TRUE(r.hasPlane("trajectory")); // 还有 b
|
||||||
|
r.onUnchecked("trajectory", "b");
|
||||||
|
EXPECT_FALSE(r.hasPlane("trajectory")); // 全消 → 平面消失
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, RecheckAfterEmptyResetsZ) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
r.onUnchecked("trajectory", "a");
|
||||||
|
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "c", 7.0), 7.0); // 重新首勾 → 新 z
|
||||||
|
}
|
||||||
|
TEST(PlaneZRegistry, SetPlaneZMovesPlane) {
|
||||||
|
PlaneZRegistry r;
|
||||||
|
r.onChecked("trajectory", "a", 12.0);
|
||||||
|
r.setPlaneZ("trajectory", 30.0);
|
||||||
|
EXPECT_DOUBLE_EQ(r.planeZ("trajectory"), 30.0);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "controller/DatasetRenderStrategy.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::controller;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct FakeStrategy : IDatasetRenderStrategy {
|
||||||
|
int added = 0;
|
||||||
|
void add(const std::string&, const std::string&) override { ++added; }
|
||||||
|
void remove(const std::string&) override {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RenderStrategyRegistry, ResolvesById) {
|
||||||
|
RenderStrategyRegistry reg;
|
||||||
|
reg.registerStrategy("fake", std::make_unique<FakeStrategy>());
|
||||||
|
auto* s = reg.get("fake");
|
||||||
|
ASSERT_NE(s, nullptr);
|
||||||
|
s->add("trajectory", "d1");
|
||||||
|
EXPECT_EQ(static_cast<FakeStrategy*>(s)->added, 1);
|
||||||
|
EXPECT_EQ(reg.get("missing"), nullptr);
|
||||||
|
}
|
||||||
|
|
@ -203,26 +203,27 @@ struct FakeSceneRepo : data::I3dSceneRepository {
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// 2D 模式 + 勾选 1 ds → 1 个测线 actor,无帘面/体素/地形。
|
// B2 后勾选统一入口 = (dsId, typeId=描述符 id) 列表。便捷构造:电阻率(curtain)/三维体(volume)/轨迹(plane2d)。
|
||||||
TEST(VtkSceneController, Map2DWithOneDatasetAddsSurveyLine) {
|
namespace {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
using IdType = std::vector<std::pair<std::string, std::string>>;
|
||||||
VtkSceneController c(ds, sc, view);
|
IdType curtainIds(std::initializer_list<std::string> ids) {
|
||||||
c.setViewMode(ViewMode::Map2D);
|
IdType v;
|
||||||
c.setCheckedDatasets({"ds1"});
|
for (const auto& id : ids) v.push_back({id, "resistivity"}); // resistivity → renderStrategyId "curtain"
|
||||||
|
return v;
|
||||||
EXPECT_EQ(view.surveyLines, 1);
|
|
||||||
EXPECT_EQ(view.curtains, 0);
|
|
||||||
EXPECT_EQ(view.volumes, 0);
|
|
||||||
EXPECT_GE(view.renders, 1);
|
|
||||||
EXPECT_TRUE(view.lastIs2D);
|
|
||||||
}
|
}
|
||||||
|
IdType voxelIds(std::initializer_list<std::string> ids) {
|
||||||
|
IdType v;
|
||||||
|
for (const auto& id : ids) v.push_back({id, "voxel"}); // voxel → "volume"
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// 3D 模式 + 帘面图层 → 1 帘面 actor。
|
// 3D 帘面:勾选电阻率(curtain 策略) → 1 帘面 actor。
|
||||||
TEST(VtkSceneController, View3DCurtainAddsCurtain) {
|
TEST(VtkSceneController, View3DCurtainAddsCurtain) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(curtainIds({"ds1"}));
|
||||||
|
|
||||||
EXPECT_EQ(view.curtains, 1);
|
EXPECT_EQ(view.curtains, 1);
|
||||||
EXPECT_EQ(view.surveyLines, 0);
|
EXPECT_EQ(view.surveyLines, 0);
|
||||||
|
|
@ -235,7 +236,7 @@ TEST(VtkSceneController, View3DVolumeDatasetAddsVolume) {
|
||||||
sc.volumeIds = {"ds1"}; // ds1 = 三维体类型 → 体素渲染路径
|
sc.volumeIds = {"ds1"}; // ds1 = 三维体类型 → 体素渲染路径
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(voxelIds({"ds1"}));
|
||||||
|
|
||||||
EXPECT_EQ(view.volumes, 1);
|
EXPECT_EQ(view.volumes, 1);
|
||||||
EXPECT_EQ(view.curtains, 0); // 体素数据集不再同时出帘面
|
EXPECT_EQ(view.curtains, 0); // 体素数据集不再同时出帘面
|
||||||
|
|
@ -247,18 +248,18 @@ TEST(VtkSceneController, View3DWithTerrainAddsTerrain) {
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setLayer(SceneLayer::Terrain, true);
|
c.setLayer(SceneLayer::Terrain, true);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(curtainIds({"ds1"}));
|
||||||
|
|
||||||
EXPECT_EQ(view.terrains, 1);
|
EXPECT_EQ(view.terrains, 1);
|
||||||
EXPECT_EQ(view.curtains, 1);
|
EXPECT_EQ(view.curtains, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消勾选 → 增量移除该 ds 图元(不整场 clear,3D 增量路径)。
|
// 取消勾选 → 增量移除该 ds 图元(不整场 clear,增量路径)。
|
||||||
TEST(VtkSceneController, UncheckRemovesDatasetIncrementally) {
|
TEST(VtkSceneController, UncheckRemovesDatasetIncrementally) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(curtainIds({"ds1"}));
|
||||||
ASSERT_EQ(view.curtains, 1);
|
ASSERT_EQ(view.curtains, 1);
|
||||||
const int clearsAfterCheck = view.clears;
|
const int clearsAfterCheck = view.clears;
|
||||||
|
|
||||||
|
|
@ -273,11 +274,11 @@ TEST(VtkSceneController, IncrementalAddKeepsExisting) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(curtainIds({"ds1"}));
|
||||||
const int clearsAfterFirst = view.clears;
|
const int clearsAfterFirst = view.clears;
|
||||||
ASSERT_EQ(view.curtains, 1);
|
ASSERT_EQ(view.curtains, 1);
|
||||||
|
|
||||||
c.setCheckedDatasets({"ds1", "ds2"}); // 增量加 ds2
|
c.setCheckedDatasets(curtainIds({"ds1", "ds2"})); // 增量加 ds2
|
||||||
EXPECT_EQ(view.curtains, 2);
|
EXPECT_EQ(view.curtains, 2);
|
||||||
EXPECT_EQ(view.clears, clearsAfterFirst); // 不重建 → 无新 clear
|
EXPECT_EQ(view.clears, clearsAfterFirst); // 不重建 → 无新 clear
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +289,7 @@ TEST(VtkSceneController, VerticalExaggerationForwarded) {
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setVerticalExaggeration(3.5);
|
c.setVerticalExaggeration(3.5);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(curtainIds({"ds1"}));
|
||||||
EXPECT_DOUBLE_EQ(view.ve, 3.5);
|
EXPECT_DOUBLE_EQ(view.ve, 3.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,7 +298,7 @@ TEST(VtkSceneController, MultipleDatasetsAddMultipleCurtains) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1", "ds2", "ds3"});
|
c.setCheckedDatasets(curtainIds({"ds1", "ds2", "ds3"}));
|
||||||
EXPECT_EQ(view.curtains, 3);
|
EXPECT_EQ(view.curtains, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,7 +310,7 @@ TEST(VtkSceneController, SetVolumeColorScaleRebuildsCheckedVolume) {
|
||||||
sc.volumeIds = {"ds1"};
|
sc.volumeIds = {"ds1"};
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1"});
|
c.setCheckedDatasets(voxelIds({"ds1"}));
|
||||||
ASSERT_EQ(view.volumes, 1);
|
ASSERT_EQ(view.volumes, 1);
|
||||||
const int removesBefore = view.removeCalls;
|
const int removesBefore = view.removeCalls;
|
||||||
|
|
||||||
|
|
@ -330,7 +331,7 @@ TEST(VtkSceneController, SetVolumeColorScalePersistsAcrossRecheck) {
|
||||||
sc.volumeIds = {"ds1"};
|
sc.volumeIds = {"ds1"};
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"ds1"}); // 加载体(填充 volumeCache_)
|
c.setCheckedDatasets(voxelIds({"ds1"})); // 加载体(填充 volumeCache_)
|
||||||
ASSERT_EQ(view.lastVolumeScale.stopCount(), 2u); // 初始两段
|
ASSERT_EQ(view.lastVolumeScale.stopCount(), 2u); // 初始两段
|
||||||
|
|
||||||
core::ColorScale edited; // 编辑成三段
|
core::ColorScale edited; // 编辑成三段
|
||||||
|
|
@ -341,7 +342,7 @@ TEST(VtkSceneController, SetVolumeColorScalePersistsAcrossRecheck) {
|
||||||
ASSERT_EQ(view.lastVolumeScale.stopCount(), 3u);
|
ASSERT_EQ(view.lastVolumeScale.stopCount(), 3u);
|
||||||
|
|
||||||
c.setCheckedDatasets({}); // 取消勾选
|
c.setCheckedDatasets({}); // 取消勾选
|
||||||
c.setCheckedDatasets({"ds1"}); // 再勾选 → 命中缓存(含编辑后色阶)
|
c.setCheckedDatasets(voxelIds({"ds1"})); // 再勾选 → 命中缓存(含编辑后色阶)
|
||||||
EXPECT_EQ(view.lastVolumeScale.stopCount(), 3u);
|
EXPECT_EQ(view.lastVolumeScale.stopCount(), 3u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,146 +410,56 @@ TEST(VtkSceneController, ZoomAndFitForwarded) {
|
||||||
EXPECT_EQ(view.fitCalls, 1);
|
EXPECT_EQ(view.fitCalls, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 二维数据集视图:足迹平铺进 View3D ──
|
// ── 二维数据集(轨迹/足迹)经 plane2d 策略平铺进场景 ──
|
||||||
|
// B2:去 col2D + setChecked2DDatasets/set2DPlacement 公有入口,2D 与 3D 合一经统一入口
|
||||||
// 显式关闭摆放(0) → 勾选 2D 足迹不渲染(仅记录勾选)。
|
// setCheckedDatasets((dsId, typeId))。trajectory 描述符 → "plane2d" 策略 → add2DDatasetAsync。
|
||||||
TEST(VtkSceneController, TwoDPlacementOffDoesNotRender) {
|
// 摆放暂固定默认(Z=0);置/底/自定义 + analysisMode 取景基线相关用例随旧入口移除(Phase E/F 重接后补)。
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
namespace {
|
||||||
VtkSceneController c(ds, sc, view);
|
IdType trajIds(std::initializer_list<std::string> ids) {
|
||||||
c.setViewMode(ViewMode::View3D);
|
IdType v;
|
||||||
c.set2DPlacement(0, 0.0); // 显式关闭
|
for (const auto& id : ids) v.push_back({id, "trajectory"}); // trajectory → renderStrategyId "plane2d"
|
||||||
c.setChecked2DDatasets({"traj1"});
|
return v;
|
||||||
EXPECT_EQ(view.mapLines, 0);
|
|
||||||
}
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// 回归(足迹默认不渲染 bug):默认摆放=Z=0(1),与 2D视图下拉可见默认项一致 →
|
// 勾选轨迹(plane2d 策略) → 1 条 mapLine,默认摆放 worldZ=0;不影响帘面/体素计数。
|
||||||
// 仅勾选 2D 足迹(不手动调 set2DPlacement)即应在 View3D 渲染,worldZ=0。
|
TEST(VtkSceneController, TrajectoryRendersAsMapLineAtDefaultZero) {
|
||||||
TEST(VtkSceneController, TwoDDefaultPlacementRendersAtZeroOnCheck) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setChecked2DDatasets({"traj1"}); // 不调 set2DPlacement,依赖默认摆放
|
c.setCheckedDatasets(trajIds({"traj1"}));
|
||||||
EXPECT_EQ(view.mapLines, 1);
|
|
||||||
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 摆放 Z=0(1) + 勾选足迹 → 1 条 mapLine,worldZ=0;不影响帘面/体素计数。
|
|
||||||
TEST(VtkSceneController, TwoDPlacementZeroAddsMapLine) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
|
||||||
VtkSceneController c(ds, sc, view);
|
|
||||||
c.setViewMode(ViewMode::View3D);
|
|
||||||
c.set2DPlacement(1, 0.0); // Z=0
|
|
||||||
c.setChecked2DDatasets({"traj1"});
|
|
||||||
EXPECT_EQ(view.mapLines, 1);
|
EXPECT_EQ(view.mapLines, 1);
|
||||||
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0);
|
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0);
|
||||||
EXPECT_EQ(view.curtains, 0);
|
EXPECT_EQ(view.curtains, 0);
|
||||||
EXPECT_EQ(view.volumes, 0);
|
EXPECT_EQ(view.volumes, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 顶部/底部摆放锚定真实地表高程:worldZ = zRefElev ± 偏移(而非世界 0 ± 偏移)。
|
// 取消勾选轨迹 → 增量移除该足迹图元(不整场 clear)。
|
||||||
TEST(VtkSceneController, TwoDPlacementTopBottomAnchorToSurfaceElev) {
|
TEST(VtkSceneController, TrajectoryUncheckRemovesMapLine) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
|
||||||
view.refElev = 1200.0; // 地表高程基准
|
|
||||||
VtkSceneController c(ds, sc, view);
|
|
||||||
c.setViewMode(ViewMode::View3D);
|
|
||||||
c.set2DPlacement(2, 0.0); // 顶部
|
|
||||||
c.setChecked2DDatasets({"traj1"});
|
|
||||||
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 1200.0 + 50.0); // 贴地表上方
|
|
||||||
c.set2DPlacement(3, 0.0); // 底部
|
|
||||||
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 1200.0 - 50.0); // 地表下方
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消勾选 2D 足迹 → 增量移除该足迹图元(不整场 clear)。
|
|
||||||
TEST(VtkSceneController, TwoDUncheckRemovesMapLine) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.set2DPlacement(1, 0.0);
|
c.setCheckedDatasets(trajIds({"traj1"}));
|
||||||
c.setChecked2DDatasets({"traj1"});
|
|
||||||
ASSERT_EQ(view.mapLines, 1);
|
ASSERT_EQ(view.mapLines, 1);
|
||||||
const int clearsBefore = view.clears;
|
const int clearsBefore = view.clears;
|
||||||
|
|
||||||
c.setChecked2DDatasets({}); // 取消勾选
|
c.setCheckedDatasets({}); // 取消勾选
|
||||||
EXPECT_EQ(view.mapLines, 0);
|
EXPECT_EQ(view.mapLines, 0);
|
||||||
EXPECT_EQ(view.clears, clearsBefore); // 增量移除,不整场 clear
|
EXPECT_EQ(view.clears, clearsBefore); // 增量移除,不整场 clear
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2D 足迹与 3D 帘面共存且独立:勾选剖面 + 足迹,各出各的图元,互不影响。
|
// 轨迹足迹与 3D 帘面经同一入口共存且独立:各出各的图元,取消足迹不影响帘面。
|
||||||
TEST(VtkSceneController, TwoDFootprintCoexistsWith3DCurtain) {
|
TEST(VtkSceneController, TrajectoryCoexistsWith3DCurtain) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
VtkSceneController c(ds, sc, view);
|
VtkSceneController c(ds, sc, view);
|
||||||
c.setViewMode(ViewMode::View3D);
|
c.setViewMode(ViewMode::View3D);
|
||||||
c.setCheckedDatasets({"prof1"}); // 3D 帘面
|
IdType both = curtainIds({"prof1"});
|
||||||
c.set2DPlacement(1, 0.0);
|
both.push_back({"traj1", "trajectory"});
|
||||||
c.setChecked2DDatasets({"traj1"}); // 2D 足迹
|
c.setCheckedDatasets(both); // 帘面 + 足迹(统一入口并集)
|
||||||
EXPECT_EQ(view.curtains, 1);
|
EXPECT_EQ(view.curtains, 1);
|
||||||
EXPECT_EQ(view.mapLines, 1);
|
EXPECT_EQ(view.mapLines, 1);
|
||||||
|
|
||||||
c.setChecked2DDatasets({}); // 取消足迹 → 帘面不受影响
|
c.setCheckedDatasets(curtainIds({"prof1"})); // 仅留帘面 → 足迹移除
|
||||||
EXPECT_EQ(view.mapLines, 0);
|
EXPECT_EQ(view.mapLines, 0);
|
||||||
EXPECT_EQ(view.curtains, 1);
|
EXPECT_EQ(view.curtains, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回归(BUG3:二维分析切回三维分析后,三维数据"不知生成到哪",要手动适配才定位):
|
|
||||||
// 二维勾选足迹自动取景后 hadArrivedData_=true;切回三维前 onAnalysisModeChanged(false) 按"三维栏空"
|
|
||||||
// 复位取景基线 → 勾选三维数据应自动取景(fitView),而非停在旧相机。
|
|
||||||
TEST(VtkSceneController, ThreeDDataFitsAfterSwitchingBackFrom2D) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
|
||||||
VtkSceneController c(ds, sc, view);
|
|
||||||
c.setViewMode(ViewMode::View3D);
|
|
||||||
|
|
||||||
c.onAnalysisModeChanged(true); // 切到二维(2D 栏空 → 基线允许取景)
|
|
||||||
c.setChecked2DDatasets({"traj1"});
|
|
||||||
ASSERT_EQ(view.mapLines, 1);
|
|
||||||
const int fitsAfter2D = view.fitCalls;
|
|
||||||
EXPECT_GE(fitsAfter2D, 1); // 足迹首次到场已取景
|
|
||||||
|
|
||||||
c.onAnalysisModeChanged(false); // 切回三维(3D 栏空 → 基线允许取景)
|
|
||||||
c.setCheckedDatasets({"prof1"});
|
|
||||||
EXPECT_EQ(view.curtains, 1);
|
|
||||||
EXPECT_GT(view.fitCalls, fitsAfter2D); // 三维数据到场自动取景(修复前不取景)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回归(二维分析下已有隐藏 3D 数据时,勾选首条足迹也应取景;旧 wasEmpty 逻辑因 3D 非空而漏取景):
|
|
||||||
TEST(VtkSceneController, TwoDFootprintFitsEvenWhenHidden3DExists) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
|
||||||
VtkSceneController c(ds, sc, view);
|
|
||||||
c.setViewMode(ViewMode::View3D);
|
|
||||||
c.setCheckedDatasets({"prof1"}); // 三维数据(取景一次)
|
|
||||||
const int fitsAfter3D = view.fitCalls;
|
|
||||||
|
|
||||||
c.onAnalysisModeChanged(true); // 切到二维(2D 栏空 → 基线允许取景)
|
|
||||||
c.setChecked2DDatasets({"traj1"});
|
|
||||||
EXPECT_EQ(view.mapLines, 1);
|
|
||||||
EXPECT_GT(view.fitCalls, fitsAfter3D); // 首条足迹取景(旧逻辑因有隐藏 3D 而漏)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义摆放(4) → worldZ=customZ;改摆放重摆已勾选足迹(移除旧 + 按新 Z 重加)。
|
|
||||||
TEST(VtkSceneController, TwoDPlacementCustomZAndReplace) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
|
||||||
VtkSceneController c(ds, sc, view);
|
|
||||||
c.setViewMode(ViewMode::View3D);
|
|
||||||
c.set2DPlacement(4, 123.5); // 自定义 Z
|
|
||||||
c.setChecked2DDatasets({"traj1"});
|
|
||||||
ASSERT_EQ(view.mapLines, 1);
|
|
||||||
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 123.5);
|
|
||||||
const int removesBefore = view.removeCalls;
|
|
||||||
|
|
||||||
c.set2DPlacement(4, 200.0); // 改自定义 Z → 重摆
|
|
||||||
EXPECT_EQ(view.mapLines, 1); // 移除 1 + 新增 1 → 净计数不变
|
|
||||||
EXPECT_EQ(view.removeCalls, removesBefore + 1); // 旧足迹被移除
|
|
||||||
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 200.0); // 新 Z 已下发
|
|
||||||
}
|
|
||||||
|
|
||||||
// 摆放从关闭(0)切到 Z=0(1) → 已勾选但未渲染的足迹补画。
|
|
||||||
TEST(VtkSceneController, TwoDPlacementOffToOnDrawsCheckedFootprint) {
|
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
|
||||||
VtkSceneController c(ds, sc, view);
|
|
||||||
c.setViewMode(ViewMode::View3D);
|
|
||||||
c.set2DPlacement(0, 0.0); // 显式关闭(默认已是 Z=0)
|
|
||||||
c.setChecked2DDatasets({"traj1"}); // 关闭态:仅记录
|
|
||||||
ASSERT_EQ(view.mapLines, 0);
|
|
||||||
|
|
||||||
c.set2DPlacement(1, 0.0); // 切到 Z=0 → 补画
|
|
||||||
EXPECT_EQ(view.mapLines, 1);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,31 @@ TEST(Api3dRepo, RegisterRadarDatasetRoutesAsDdRadar3d) {
|
||||||
std::filesystem::remove_all(dir, ec);
|
std::filesystem::remove_all(dir, ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isRadarVolume:仅带 linePrefix 的体(registerRadarDataset 登记)为真;普通 IDW 体、未知 id 为假。
|
||||||
|
// 支撑「沿线位置」滑块门控(仅雷达体在场才显示)。
|
||||||
|
TEST(Api3dRepo, IsRadarVolumeOnlyTrueForRadarLine) {
|
||||||
|
const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_isradar";
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove_all(dir, ec);
|
||||||
|
writeSyntheticRadarLine(dir);
|
||||||
|
|
||||||
|
StubAsyncRepo dsRepo;
|
||||||
|
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(22.0, 114.0);
|
||||||
|
Api3dRepository repo(dsRepo, frame);
|
||||||
|
|
||||||
|
const std::string radarId =
|
||||||
|
repo.registerRadarDataset(dir.string(), "L", "测线L", "tm-1", /*coarse=*/1);
|
||||||
|
EXPECT_TRUE(repo.isRadarVolume(radarId));
|
||||||
|
|
||||||
|
VolumeBuildParams p;
|
||||||
|
p.sourceDatasetIds = {"src-a"};
|
||||||
|
const std::string plainId = repo.createVolume(p, "普通体");
|
||||||
|
EXPECT_FALSE(repo.isRadarVolume(plainId)); // 普通 IDW 体无 linePrefix
|
||||||
|
EXPECT_FALSE(repo.isRadarVolume("no-such")); // 未知 id
|
||||||
|
|
||||||
|
std::filesystem::remove_all(dir, ec);
|
||||||
|
}
|
||||||
|
|
||||||
// loadVolume:首次勾选时懒建雷达体(无 QCoreApplication → 同步交付;全量测试中若其它用例已建
|
// loadVolume:首次勾选时懒建雷达体(无 QCoreApplication → 同步交付;全量测试中若其它用例已建
|
||||||
// QCoreApplication 单例则走异步,processEvents 排空队列交付)。回调收到有效 VolumeGrid。
|
// QCoreApplication 单例则走异步,processEvents 排空队列交付)。回调收到有效 VolumeGrid。
|
||||||
TEST(Api3dRepo, LoadVolumeBuildsRadarLazily) {
|
TEST(Api3dRepo, LoadVolumeBuildsRadarLazily) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "repo/CategoryDescriptor.hpp"
|
||||||
|
#include "DatasetCategory.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::data;
|
||||||
|
|
||||||
|
TEST(CategoryCatalog, HasFiveSegmentsInOrder) {
|
||||||
|
const auto& cat = categoryCatalog();
|
||||||
|
ASSERT_EQ(cat.size(), 5u);
|
||||||
|
EXPECT_EQ(cat[0].id, "resistivity");
|
||||||
|
EXPECT_EQ(cat[3].id, "voxel");
|
||||||
|
EXPECT_EQ(cat[4].id, "trajectory");
|
||||||
|
EXPECT_EQ(cat[4].sceneKind, SceneKind::Plane2D);
|
||||||
|
EXPECT_EQ(cat[4].renderStrategyId, "plane2d");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CategoryCatalog, TrajectoryClassifiesByDdCode) {
|
||||||
|
const auto& cat = categoryCatalog();
|
||||||
|
DsRow traj; traj.id = "t1"; traj.ddCode = "dd_trajectory_data";
|
||||||
|
EXPECT_TRUE(cat[4].classify(traj));
|
||||||
|
DsRow vox; vox.ddCode = "dd_voxel";
|
||||||
|
EXPECT_FALSE(cat[4].classify(vox));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SplitByCategory, RoutesRowToFirstMatchingDescriptor) {
|
||||||
|
DsRow traj; traj.id = "t1"; traj.ddCode = "dd_trajectory_data";
|
||||||
|
DsRow ert; ert.id = "e1"; ert.dsTypeCode = "ERT platform inversion data";
|
||||||
|
auto b = geopro::app::splitByCategory({traj, ert});
|
||||||
|
const auto& cat = categoryCatalog();
|
||||||
|
ASSERT_EQ(b.segments.size(), cat.size());
|
||||||
|
EXPECT_EQ(b.segments[0].size(), 1u); // resistivity ← ert
|
||||||
|
EXPECT_EQ(b.segments[0][0].id, "e1");
|
||||||
|
EXPECT_EQ(b.segments[4].size(), 1u); // trajectory ← traj
|
||||||
|
EXPECT_EQ(b.segments[4][0].id, "t1");
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,29 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <clocale>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
#include "core/algo/GprVolumeBuilder.hpp"
|
#include "core/algo/GprVolumeBuilder.hpp"
|
||||||
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
|
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 在指定目录写出一组最小可解析的规范化 .head/.data(K=4 道 M=2 通道 N=3 采样)。
|
||||||
|
void writeMinimalLine(const fs::path& head, const fs::path& data) {
|
||||||
|
{ std::ofstream f(head);
|
||||||
|
f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n"
|
||||||
|
"DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; }
|
||||||
|
{ std::ofstream f(data, std::ios::binary);
|
||||||
|
for (int t = 0; t < 4; ++t) for (int c = 0; c < 2; ++c) for (int s = 0; s < 3; ++s) {
|
||||||
|
std::int16_t v = static_cast<std::int16_t>(t * 10 + c * 100 + s);
|
||||||
|
f.write(reinterpret_cast<const char*>(&v), 2); } }
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
|
TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
|
||||||
// K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。
|
// K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。
|
||||||
fs::path dir = fs::temp_directory_path() / "radar_bridge_test";
|
fs::path dir = fs::temp_directory_path() / "radar_bridge_test";
|
||||||
|
|
@ -26,3 +44,55 @@ TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
|
||||||
EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0
|
EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0
|
||||||
EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2
|
EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 回归:中文目录路径必须能打开渲染。app 传入的是 QString::toLocal8Bit(),即当前
|
||||||
|
// ANSI 代码页(简中=GBK)窄字节。关键复现条件——GUI app 链接 QWebEngine(Chromium)/VTK,
|
||||||
|
// 它们在启动时 setlocale(LC_ALL,"") 把 LC_CTYPE 提升为系统 UTF-8 locale;此后窄字符
|
||||||
|
// ifstream 会把 GBK 路径字节当 UTF-8 解析 → open 失败(即"打开 .head 失败")。
|
||||||
|
// 故本测试显式置 UTF-8 locale 复现该失败面,走宽字符打开(见 io/gpr/LocalPath)守护回归。
|
||||||
|
// (纯 "C" locale 下 UCRT 用 CP_ACP=GBK 解窄路径,反而不失败,无法复现——须置 UTF-8。)
|
||||||
|
TEST(NormalizedRadarBridge, OpensCjkDirectoryPathUnderUtf8Locale) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
const std::wstring wname = L"radar_cjk_南同大道";
|
||||||
|
const fs::path dir = fs::temp_directory_path() / wname;
|
||||||
|
fs::remove_all(dir);
|
||||||
|
fs::create_directories(dir);
|
||||||
|
writeMinimalLine(dir / L"南同大道_000.head", dir / L"南同大道_000.data");
|
||||||
|
|
||||||
|
// 模拟 app:宽字符 → 当前 ANSI 代码页(GBK)窄字节,等价 QString::toLocal8Bit()。
|
||||||
|
auto toAcp = [](const std::wstring& w) {
|
||||||
|
const int n = ::WideCharToMultiByte(CP_ACP, 0, w.data(),
|
||||||
|
static_cast<int>(w.size()), nullptr, 0,
|
||||||
|
nullptr, nullptr);
|
||||||
|
std::string s(static_cast<std::size_t>(n), '\0');
|
||||||
|
::WideCharToMultiByte(CP_ACP, 0, w.data(), static_cast<int>(w.size()),
|
||||||
|
s.data(), n, nullptr, nullptr);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
const std::string dirAcp = toAcp(dir.wstring());
|
||||||
|
const std::string prefixAcp = toAcp(L"南同大道_000");
|
||||||
|
|
||||||
|
// 复现 app 运行期的 UTF-8 C locale(QWebEngine/VTK 所置)——不修复则 narrow open 失败。
|
||||||
|
const char* prevC = std::setlocale(LC_ALL, nullptr);
|
||||||
|
const std::string savedC = prevC ? prevC : "C";
|
||||||
|
std::setlocale(LC_ALL, ".UTF-8");
|
||||||
|
|
||||||
|
geopro::core::BuiltI16 b;
|
||||||
|
try {
|
||||||
|
b = geopro::io::gpr::buildLineVolumeFromNormalized(
|
||||||
|
dirAcp, prefixAcp, /*coarse=*/1, /*targetDy=*/0.0);
|
||||||
|
} catch (...) {
|
||||||
|
std::setlocale(LC_ALL, savedC.c_str());
|
||||||
|
fs::remove_all(dir);
|
||||||
|
throw; // 未修复时会在此抛"打开 .head 失败"→ 测试红,正是回归守护
|
||||||
|
}
|
||||||
|
std::setlocale(LC_ALL, savedC.c_str());
|
||||||
|
|
||||||
|
EXPECT_EQ(b.vol.nx(), 4);
|
||||||
|
EXPECT_EQ(b.vol.ny(), 2);
|
||||||
|
EXPECT_EQ(b.vol.nz(), 3);
|
||||||
|
fs::remove_all(dir);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "中文窄路径打开问题仅 Windows 相关";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,73 @@ TEST(CameraPreset, ZoomInOrthoReducesParallelScale) {
|
||||||
EXPECT_LT(c->GetParallelScale(), before);
|
EXPECT_LT(c->GetParallelScale(), before);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── orbitPose(纯数学,供 orbitToAxis 用)──────────────────────────────────────
|
||||||
|
// 各方向:focal==pivot、|pos-pivot|==distance、pos 沿正确轴偏移、up 与 applyView 约定一致。
|
||||||
|
namespace {
|
||||||
|
constexpr double kPivot[3] = {10.0, -20.0, 5.0};
|
||||||
|
constexpr double kDist = 7.0;
|
||||||
|
|
||||||
|
double dist3(const double a[3], const double b[3]) {
|
||||||
|
const double dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
|
||||||
|
return std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Top:pos 在 pivot 的 +Z、距离保持,up=+Y(对齐 applyView Top)。
|
||||||
|
TEST(OrbitPose, TopOffsetsPlusZUpY) {
|
||||||
|
auto pose = orbitPose(ViewDir::Top, kPivot, kDist);
|
||||||
|
EXPECT_NEAR(pose.focal[0], kPivot[0], 1e-9);
|
||||||
|
EXPECT_NEAR(pose.focal[1], kPivot[1], 1e-9);
|
||||||
|
EXPECT_NEAR(pose.focal[2], kPivot[2], 1e-9);
|
||||||
|
EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[0], kPivot[0], 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[1], kPivot[1], 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[2], kPivot[2] + kDist, 1e-9); // +Z
|
||||||
|
EXPECT_NEAR(pose.up[0], 0.0, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.up[1], 1.0, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.up[2], 0.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom:pos 在 -Z,up=+Y。
|
||||||
|
TEST(OrbitPose, BottomOffsetsMinusZUpY) {
|
||||||
|
auto pose = orbitPose(ViewDir::Bottom, kPivot, kDist);
|
||||||
|
EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[2], kPivot[2] - kDist, 1e-9); // -Z
|
||||||
|
EXPECT_NEAR(pose.up[1], 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Front:从 -Y 看 +Y → pos 在 -Y,up=+Z。
|
||||||
|
TEST(OrbitPose, FrontOffsetsMinusYUpZ) {
|
||||||
|
auto pose = orbitPose(ViewDir::Front, kPivot, kDist);
|
||||||
|
EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[1], kPivot[1] - kDist, 1e-9); // -Y
|
||||||
|
EXPECT_NEAR(pose.up[2], 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back:pos 在 +Y,up=+Z。
|
||||||
|
TEST(OrbitPose, BackOffsetsPlusYUpZ) {
|
||||||
|
auto pose = orbitPose(ViewDir::Back, kPivot, kDist);
|
||||||
|
EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[1], kPivot[1] + kDist, 1e-9); // +Y
|
||||||
|
EXPECT_NEAR(pose.up[2], 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left:从 -X 看 +X → pos 在 -X,up=+Z。
|
||||||
|
TEST(OrbitPose, LeftOffsetsMinusXUpZ) {
|
||||||
|
auto pose = orbitPose(ViewDir::Left, kPivot, kDist);
|
||||||
|
EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[0], kPivot[0] - kDist, 1e-9); // -X
|
||||||
|
EXPECT_NEAR(pose.up[2], 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right:pos 在 +X,up=+Z。
|
||||||
|
TEST(OrbitPose, RightOffsetsPlusXUpZ) {
|
||||||
|
auto pose = orbitPose(ViewDir::Right, kPivot, kDist);
|
||||||
|
EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9);
|
||||||
|
EXPECT_NEAR(pose.pos[0], kPivot[0] + kDist, 1e-9); // +X
|
||||||
|
EXPECT_NEAR(pose.up[2], 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
// 空指针/非法 factor 安全。
|
// 空指针/非法 factor 安全。
|
||||||
TEST(CameraPreset, NullAndInvalidAreSafe) {
|
TEST(CameraPreset, NullAndInvalidAreSafe) {
|
||||||
applyView(nullptr, ViewDir::Top);
|
applyView(nullptr, ViewDir::Top);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue