Compare commits
31 Commits
cdd7613d53
...
9b4f172809
| Author | SHA1 | Date |
|---|---|---|
|
|
9b4f172809 | |
|
|
d5e3522bfa | |
|
|
fadcd12239 | |
|
|
cf1c06cde8 | |
|
|
5bf3a8e5dd | |
|
|
bdebe54859 | |
|
|
6a10975b6b | |
|
|
4e998374e7 | |
|
|
227ee8fdef | |
|
|
c1a824e292 | |
|
|
e8bb2f82e7 | |
|
|
1648ccb8c4 | |
|
|
f230ca8dd1 | |
|
|
9782a2b93e | |
|
|
306d7bc46e | |
|
|
d7ab7705c9 | |
|
|
91a71064b2 | |
|
|
302d946bd9 | |
|
|
b48684a0ba | |
|
|
1a70ca0072 | |
|
|
4ae8286bb0 | |
|
|
d470dc8154 | |
|
|
04af569b7d | |
|
|
3ed1ea75ac | |
|
|
58544ffb3c | |
|
|
c6756aafc5 | |
|
|
75c1327aa4 | |
|
|
56e4b3a7ff | |
|
|
85636931af | |
|
|
d6e52cb51f | |
|
|
fb911a9d85 |
|
|
@ -55,3 +55,22 @@
|
||||||
落在光标下那张切片 → 选对(`onPick` 仅命中时触发,未命中不误选)。重叠切片仍按最前优先(合理)。
|
落在光标下那张切片 → 选对(`onPick` 仅命中时触发,未命中不误选)。重叠切片仍按最前优先(合理)。
|
||||||
逻辑闭合但**未 live 点击验证**(工具无法交互点击 3D 切片);若仍有偏差需 live 复核(重叠循环切换等)。
|
逻辑闭合但**未 live 点击验证**(工具无法交互点击 3D 切片);若仍有偏差需 live 复核(重叠循环切换等)。
|
||||||
- **更新**:2026-06-25 issue2+③+反向²(`69e8790`)+④(`63cda56`) 全部实现。
|
- **更新**:2026-06-25 issue2+③+反向²(`69e8790`)+④(`63cda56`) 全部实现。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OPT-003 · 二维分析 C 期:dd_raster 栅格地理配准渲染(阻塞·待后端端点)
|
||||||
|
- **状态**:🔴 Open(**阻塞**:后端无栅格数据端点)
|
||||||
|
- **记录日期**:2026-06-26
|
||||||
|
- **背景/现状**:二维分析改造分期 A→B→C(spec `docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`)。
|
||||||
|
A(一场景两相机)、B(足迹高程 Z 拖动)已实现(commit `6a10975`、`bdebe54`)。**C 期=dd_raster 栅格**
|
||||||
|
(DD0623 新增 ddCode,展示模式 2D,形态=栅格/遥感影像)尚未做。
|
||||||
|
- **期望**:`dd_raster` 纳入 2D 维度过滤(`dimOf`/`dimensionOf` 加 `dd_raster`→Dim2D);col2D 勾选渲染按
|
||||||
|
ddCode 分派(轨迹走 `loadMapLine`,栅格走**栅格加载**,不可串);栅格取**像素 + 四至/仿射 + 投影 CRS**
|
||||||
|
作地理配准纹理平面贴到地形上(带高程,可被 B 期 Z 拖动),类似底图瓦片按经纬定位。
|
||||||
|
- **阻塞点(为何不做)**:实测后端 API 文档 `docs/apis/business_OpenAPI.json` **无任何 dd_raster / 栅格影像
|
||||||
|
端点**(仅有 grid 行/反演 grid、GPR 通道图,均非带四至+投影的栅格)。无端点 → 无像素/地理范围可加载 →
|
||||||
|
C 期无数据可渲染。**须后端提供返回「像素 + 四至/仿射 + 投影」的端点后方可落地。**
|
||||||
|
- **难点**:栅格加载路径(新)、按 ddCode 的渲染分派、地理配准纹理平面(参考 `TileBasemap` 的 `buildFlat`/
|
||||||
|
`buildWarped` 按经纬贴地形 + DEM 位移)。
|
||||||
|
- **关联**:spec §6/§9/§10/§11;handoff `docs/superpowers/HANDOFF-2026-06-26-vtk-anomaly-2d-analysis.md` §0。
|
||||||
|
- **更新**:—
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
# HANDOFF — VTK 3D 视图:创建异常打磨 + 切片/异常交互 + 二维分析改造(2026-06-26)
|
||||||
|
|
||||||
|
> 分支 `feat/vtk-3d-view`。桌面端 Qt6 + VTK 9.6。本会话围绕「创建异常」全链路打磨、切片/异常交互修复、全局中文化,最后转入「二维分析改造」的需求厘清 + 写 spec。**下一步=按 spec 实现二维分析 A 期。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 立刻要做的事(下个会话从这里开始)
|
||||||
|
**二维分析改造 A 期已实现**(未提交,下个会话需用户实跑反馈手感/角度)。spec:`docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`(commit `227ee8f`)。分期 A→B→C:
|
||||||
|
- **A(已实现 ✅,build+439测试全绿,未提交)**:一场景两相机。切「二维分析」tab → 近俯视(下压12°≈78°俯角)+禁旋转(左键改平移、仅平移/缩放);按维度翻 actor `SetVisibility`(轨迹↔体/帘面/异常,**不清空**);切片 `SetEnabled` 显隐(不销毁);地形+底图常驻;切回三维还原相机快照。**待用户实跑**:①近俯视角度是否合适②切换是否瞬时③左键平移手感④切回三维视角还原是否自然。
|
||||||
|
- 改动文件:`CameraPreset.{hpp,cpp}`(applyNearTop2D)、`PickInteractorStyle.{hpp,cpp}`(setLock2D)、`SliceTool.{hpp,cpp}`(setVisible)、`InteractionManager.{hpp,cpp}`(setMode2D)、`VtkSceneView.{hpp,cpp}`(setAnalysisMode2D+mapLineDs_+相机快照)、`ColumnDrawer.{hpp,cpp}`(analysisModeChanged 信号)、`main.cpp`(接信号)。
|
||||||
|
- 已知小风险:2D 取景 `computeDataBounds` 含隐藏的 3D 体包围盒(地形主导,影响小);切片 `SetEnabled` 显隐属 GUI 不可自测项。
|
||||||
|
- **B(已实现 ✅,build+441测试全绿,未提交,待实跑)**:二维里选中足迹(单/Ctrl 多选)→ 竖向拖动只改**高程 Z**、锁 XY、顶部实时高程读数浮层;Z 偏移按 dsId 持久(切走再回/全量重建保留)。手势:单击足迹=选中、Ctrl+单击=多选切换、点空白=取消+平移、(多)选后竖向拖动=整体改 Z。
|
||||||
|
- 实现:`VtkSceneView` 加 `pickMapLineAt/nudgeSelectedMapLinesZ/selectedMapLineZ/clearMapLineSelection`(vtkCellPicker+PickFromList 只拾可见足迹、选中高亮黄加粗、`mapLineZOffset_` 持久);`PickInteractorStyle` lock2D 下命中足迹→Z 拖动(`onPick2D/onDrag2D/onDrag2DEnd`+`worldPerPixelZ` 像素→世界Z)、否则平移;`InteractionManager::pickStyle()` 暴露样式;`main.cpp` 接回调 + 高程读数浮层(复用提示样式)。
|
||||||
|
- **待用户实跑**:①拾取灵敏度(tol 0.012)②拖动 Z 灵敏度/方向(上移=抬高)③多选拖动④读数是否合理(现为 actor 包围盒中心世界 Z,含 placement+偏移,未除 VE)。
|
||||||
|
- **C(下一步)**:dd_raster 纳入 2D 过滤 + 按 ddCode 分派渲染 + 栅格地理配准贴地形。**阻塞:dd_raster 数据端点未确认**(需后端给「像素 + 四至/投影」端点)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 环境 / 命令 / 铁律(必读)
|
||||||
|
- **构建**:用 PowerShell 工具跑 `cmd /c "D:\Git\lanbingtech\geopro\build.bat app"`(Claude 的 Bash 跑 build 会被环境劫持,用 PowerShell)。测试 `build.bat test`(ctest,**439 用例**)。`vswhere.exe not recognized` 噪声无害。LNK1104=exe 被运行中的 app 锁,需用户关 app 才能链接。
|
||||||
|
- **回复用中文**(用户要求)。
|
||||||
|
- **CLAUDE.md 两条绑定规则**:①发现技术债当场修,不以"非本轮引入"搪塞;②**能自己做的绝不让用户做**——日志(`%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log`)/数据/构建/诊断都自己来,只在 LNK1104 关 app、或真正产品决策才找用户。
|
||||||
|
- **git**:精确 `git add <files>`(仓库有并行 GPR 会话的脏文件 `.superpowers/sdd/*`、`docs/.../poc-lod-shots/*.png`,**勿误提交**)。提交无 Co-Authored-By(全局禁)。
|
||||||
|
- **后端 token(可访问真实接口,用户给的)**:`geomativeauthorization: Geomative e6c1259748644c8da0954d864bb82604`;base URL `http://tenant.geomative.cn/pop-api`;样例 projectId `1439735554211840`。可用 curl 查接口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本会话已完成(提交清单,新→旧)
|
||||||
|
**创建异常全链路(核心工作量)**
|
||||||
|
- `227ee8f` 二维分析 spec(doc)。
|
||||||
|
- `c1a824e` 二维维度分类对齐数据字典 DD0623(去 3 个已删除轨迹类型,dimensionOf/dimOf/测试)。
|
||||||
|
- `e8bb2f8`/`1648ccb`/`f230ca8` 异常绘制提示:vtkTextActor 渲染不出中文 → 改 **app 层 QLabel 浮层**(右上角、深底方角、不挡鼠标);列表切到别对象清切片选中(`deselectSlice`)。
|
||||||
|
- `9782a2b` 删除切片/异常加确认框 + **弹框按钮全局中文化**(Qt zh_CN 翻译器 + `formkit::addDialogButtons` 默认「确定/取消」+ 打包补 qtbase_zh_CN.qm)。
|
||||||
|
- `306d7bc` 提示移右上角 + **线双击结束含双击位置**(去回滚)。
|
||||||
|
- `d7ab770` **切片保存后定稿锁定**(`SetInteraction(0)` 不可移动/旋转)+ VTK/列表菜单去「保存·另存」。
|
||||||
|
- `91a7106` **结束手势**:点=单击即完成、线=双击、面=**点回起点闭合**(近起点 12px 吸附+橡皮筋指向起点)。
|
||||||
|
- `1a70ca0` 异常对话框加**样式预览**(选中类型 legend 可视化:点球/线/面)。
|
||||||
|
- `4ae8286` **异常截图配色与切面一致**:取 widget 自身 `GetColorMap()` 输出(非另建 LUT)→ 逐像素一致 + RGBA 外区透明消蓝边。
|
||||||
|
- `d470dc8` 双击/单击隔离(后被 `306d7bc` 改回"含双击位置")+ 异常类型下拉误显「暂无数据」(`EmptyAwareComboBox::realItemCount` 用错 flags 角色→改 `model()->flags()`)。
|
||||||
|
- `04af569` 返工(点交互/点渲染小球/截图/类型空态)。
|
||||||
|
- `75c1327`→`3ed1ea7`→`58544ff`→`c6756aa` 截图相机方案A(已废)、**异常类型接平台真实类型**(`listExceptionTypes` 按形态)、**点/线/面三态**子菜单、**样式接平台 legend**(`getExceptionTypeDetail`)。
|
||||||
|
- 截图最终方案:**只从切片 2D 剖面图、按异常几何 buffer 裁剪**(`captureAnomalyShotFromSlice`,GIS buffer+掩膜,点圆/线胶囊/面外扩多边形)。
|
||||||
|
|
||||||
|
**其它**
|
||||||
|
- `56e4b3a` 登录验证码容器白底。
|
||||||
|
- `8563693` 分段折叠向上收起(stretch 动态:展开=1/折叠=0+尾弹簧)。
|
||||||
|
- `d6e52cb` 三维分析分段面板视觉打磨(chevron 段头/描边新增按钮/顶部留白)。
|
||||||
|
- `fb911a9` 坐标轴面板硬编码色 token 化。
|
||||||
|
- `cdd7613` vtk-3d-openapi 文档对齐实测(DsPage 行在 `data.list`、DsRow 全字段、装置不在 data/page)。
|
||||||
|
- `2f6ec7d`→`1742b75`→`31ad7a4` **装置筛选**:data/page 行不带 properties→改按行自带 `typeName/dsTypeCode` 筛选;parseDsRows 兼容对象形态。
|
||||||
|
- 期间生成了一次安装包:`installer/dist/Geopro_Setup_3.0.0-20260625.exe`(脚本 `installer/build_installer.ps1`,含 windeployqt+样本+PROJ+vc_redist)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 创建异常功能现状(已落地,端到端可用,mock 保存)
|
||||||
|
入口:VTK 切片右键 →「创建异常 → 点/线/面」。流程:圈定(AnomalyDrawTool)→草稿渲染→**从切片2D图buffer裁剪截图**→对话框(名称/异常类型[接平台]/样式预览/备注/截图)→保存(mock)→刷新树+渲染异常 actor。
|
||||||
|
- **关键文件**:`src/render/interact/AnomalyDrawTool.{hpp,cpp}`(三态绘制:点单击完成/线双击/面点起点闭合,Esc取消/Backspace撤点);`src/app/AnomalySaveDialog.{hpp,cpp}`(接 `cmdRepo.listExceptionTypes`/`getExceptionTypeDetail`,样式预览);`src/app/SliceExport.{hpp,cpp}`(`captureAnomalyShotFromSlice`);`src/render/actors/AnomalyActor.cpp`(点=球/线=折线/面=闭合多边形);`src/app/main.cpp`(onSliceContextMenuRequested ~509 起:菜单+绘制+对话框+保存接线,QLabel 提示浮层)。
|
||||||
|
- **样式来源**:选中平台异常类型 → `getExceptionTypeDetail` 取 legend(polylineColor/Width/Shape, pointColor…)→ 套到异常 lineColor/lineWidth/dashed。
|
||||||
|
- **截图**:只裁切片那张 2D 剖面图(`InteractionManager::selectedSliceColorImage` 现取 `SliceTool::coloredResliceImage()`=widget ColorMap 输出,与屏幕同源)。
|
||||||
|
|
||||||
|
### 仍卡在后端(P3,非客户端问题)
|
||||||
|
- **异常真保存 `newException`** 卡:异常 `remarkSourceId` 须指向真实 dsObjectId(三维体/切片),但真后端**无任何登记三维体/切片为 dsObject 的端点**(实测 `voxel/generate`、`slice/generate`、通用 `dsObject create` 全无)。当前 `saveAnomaly` 是内存 mock。后端补登记端点后整链可接真。
|
||||||
|
- 平台「点」类异常类型该项目为空(线/面有)→ 画点时类型下拉空属正常。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 二维分析改造——已确认的设计决策(与用户逐条敲定)
|
||||||
|
1. **不分两个场景**:一个 3D 地形场景(带高程)+ 底图贴地形,两栏只是相机不同。"二维分析只是 3D 的固定视角"。
|
||||||
|
2. **二维相机**:锁定**近俯视(75–80°,非绝对正俯视)**,禁旋转,仅平移+缩放。(绝对正俯视看不出高程→留倾斜)
|
||||||
|
3. **切 tab 显隐**:翻另一方数据集 `SetVisibility`(**不显示,非清空**)→ 性能零代价(VTK 跳过不可见 actor)、切换瞬时、只占内存。地形+底图常驻。**绝不清空**(重体素重建会卡)。
|
||||||
|
4. **高程拖动(C1)**:二维里选中 2D 内容(单/多选)→ 竖向拖动只改**高程 Z**、锁 XY、实时读数。用途=分离叠在一起的 2D 层。
|
||||||
|
5. **维度过滤**:2D = `dd_trajectory_data`(+ C 期 `dd_raster`);`dd_radar_2d/3d` 是 3D(不进 2D)。已对齐 DD0623(`c1a824e`)。
|
||||||
|
6. **雷达客户反馈**:只影响数据模型 + **3D 视图**渲染(二维雷达=线、三维雷达=切面、带打标)+ 详情页校准——**均属另立任务,不在本次 2D 改造**。2D 只显示轨迹线、打标暂不做(与本设计一致)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键代码锚点(二维分析改造用)
|
||||||
|
- 切 2D 视图模式钩子:`Column2DDataset::view2DModeChanged` → `main.cpp` 接 `sceneCtrl`(搜 `view2DModeChanged`)。
|
||||||
|
- 维度分类:`src/app/DatasetDimension.cpp::dimOf`(col2D 用);`src/data/api/Api3dRepository.cpp` + `src/data/repo/LocalSample3dRepository.cpp::dimensionOf`。
|
||||||
|
- 2D 内容注入:`main.cpp` ~502 `col2D->setDatasets(splitByDimension(...).dim2D)`;勾选渲染 `Column2DDataset::checkedDatasetsChanged` → `loadMapLine`/`MapLineActor`。
|
||||||
|
- 场景/相机/可见:`VtkSceneView`(actor 管理)、`VtkSceneController`、`InteractionManager`(拾取/选中,新增 `deselectSlice`)。
|
||||||
|
- 地形+底图:`buildTerrain`(带高程,VE 垂直夸张相关)、`TileBasemap`(天地图按经纬贴)。
|
||||||
|
- 数据字典:`D:\Projects\GEOPRO\DD0623.xlsx`(46 个 ddCode,列:序号/ddCode/领域/含义/状态/**展示模式(2D/3D)**/**展示形态**/备注…)。解析:openpyxl 可用,中文 GBK 终端会乱码→导 UTF-8 文件再 Read。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 待确认/风险
|
||||||
|
- **dd_raster 数据端点**(像素+四至/投影)未确认 → C 期阻塞。
|
||||||
|
- 近俯视角度需实机调;高程是否落库暂定会话内(不落库)。
|
||||||
|
- §3 翻可见标志需可靠区分每个 actor 的维度归属。
|
||||||
|
- 切相机/可见与现有 view2DModeChanged、底图、地形 VE 逻辑勿打架。
|
||||||
|
- **GUI 无法登录自测**:相机/截图/交互类改动我只能保证编译+逻辑,视觉/手感需用户实跑反馈。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 相关记忆/文档
|
||||||
|
- 记忆库 `MEMORY.md`(如 login-chain-truth、vtk-3d-persistence-structure、build-vs2026-vcpkg-gotchas、deploy-hardcoded-dev-paths 等)。
|
||||||
|
- vtk-3d API 设计草案:`docs/api/vtk-3d-openapi.json`(已对齐实测,0.6.1)。
|
||||||
|
- 真后端 API:`docs/apis/business_OpenAPI.json`。
|
||||||
|
- 二维分析 spec:`docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`。
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
# 二维分析:锁定俯视相机 + 内容显隐 + 高程拖动 — Spec(2026-06-26)
|
||||||
|
|
||||||
|
## 0. 一句话目标
|
||||||
|
把「二维分析」从"另一套平面地图"改为**同一个 3D 地形场景的一个锁定近俯视视角**:切 tab 只切相机+翻另一方数据集的可见标志(不清空),二维内容(轨迹/栅格)落在带高程的地形上,且可选中后沿高程上下拖动分离。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与现状
|
||||||
|
- 三维分析栏(`CategoryAnalysisTab`)与二维分析栏(`Column2DDataset` / `col2D`)**共用同一个 `VtkSceneView` / `InteractionManager` / `renderWindow`**。现状:两栏勾选的 actor 叠在同一场景,切 tab 不切相机、不区分内容。
|
||||||
|
- 二维内容现状:`col2D` 勾选 → `loadMapLine`(`dd/ert/trajectory/line`)→ `MapLineActor`(lat/lon 折线);有 `view2DModeChanged` 信号已接到 `sceneCtrl`(2D 视图模式钩子,本 spec 在其上扩展)。
|
||||||
|
- 地形 + 底图:场景已有地形(`buildTerrain`,带高程)+ 天地图底图(`TileBasemap`,按经纬贴)。
|
||||||
|
- 维度分类:`DatasetDimension::dimOf` + `Api3dRepository/LocalSample3dRepository::dimensionOf`,已对齐数据字典 DD0623(2D = `dd_trajectory_data`,commit c1a824e)。
|
||||||
|
|
||||||
|
**用户确认的认知**:二维分析"只是 3D 的固定视角",底图不是平面图、是**带高程的地形图**;内容沿用不清空。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心设计:一个场景 + 两种相机
|
||||||
|
- **不分两个场景**,只有一个 3D 地形场景(地形 + 底图 + 全部已勾选数据),两栏区别仅在**相机**与**哪类数据集可见**。
|
||||||
|
|
||||||
|
| | 三维分析 | 二维分析 |
|
||||||
|
|---|---|---|
|
||||||
|
| 相机 | 自由透视(可旋转/倾斜/平移/缩放) | **锁定近俯视**(不可旋转,仅平移+缩放)|
|
||||||
|
| 可见数据集 | 3D 数据集(体/切片/剖面…)可见;2D 数据集隐藏 | 2D 数据集(轨迹/栅格)可见;3D 数据集隐藏 |
|
||||||
|
| 地形 + 底图 | 常驻可见 | 常驻可见(同一地形,俯视看即"地形图")|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 二维分析的相机:锁定近俯视
|
||||||
|
- 切到二维分析 → 相机切到**近俯视固定角(约 75–80°,非绝对正俯视)**:理由——绝对正俯视在正交/小透视下高程变化不可见,留一点倾斜以便看出高低(§5 拖动反馈需要)。
|
||||||
|
- 锁定:**禁用旋转**(interactor style 不响应旋转/倾斜),仅保留**平移 + 缩放**。
|
||||||
|
- 切回三维分析 → 恢复自由透视相机(恢复切走前的视角或合理默认)。
|
||||||
|
- 实现锚点:扩展 `view2DModeChanged` 钩子 → `VtkSceneController` 切相机模式 + 切 interactor style(或在 style 内按模式禁旋转)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 内容显隐:切 tab 翻可见标志(**不清空**)
|
||||||
|
- 切 tab 时,对"另一方"的数据集 actor 用 `SetVisibility(false)`,切回 `SetVisibility(true)`。**不移除 actor、不重建**。
|
||||||
|
- 性能:VTK 渲染跳过不可见 actor → 隐藏内容**不参与绘制、不耗 FPS**;切换瞬时(无重插值/重传 GPU);唯一代价是内存/显存驻留(数据本已加载,无新增加载)。
|
||||||
|
- **禁止用清空**:重体素(GPR/ERT)每次切回要重插值+重传 GPU,必卡。
|
||||||
|
- 地形 + 底图两边都不隐藏。
|
||||||
|
- 实现锚点:`VtkSceneView` 按数据集维度(`dimensionOf`/记录每 actor 的维度)批量翻可见标志;切 tab 时调用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 高程拖动(C1):选中 2D 内容沿 Z 上下移
|
||||||
|
- 二维分析里,**拾取选中已渲染的 2D 内容**(轨迹/栅格),支持**单选 / 多选**。
|
||||||
|
- 选中后**竖向拖动 → 仅改其高程 Z(离地高度)**,**锁死 X/Y**(不动地理位置)。用于把叠在一起的 2D 层分离、看清。
|
||||||
|
- 拖动时**实时显示当前高程数值**(屏幕浮层读数)。
|
||||||
|
- 近俯视固定角(§3)使高低可见。
|
||||||
|
- 实现锚点:新增/复用一个 2D 拾取-拖动交互(类似切片 widget 但只允许 Z 平移 + 多选);actor 的 Z 偏移持久(切走再回保留)。
|
||||||
|
- 待定:高程是否需要随数据保存(暂定仅会话内 actor 变换,不落库;接真实端点再议)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. dd_raster:二维栅格过滤 + 渲染(本期新增)
|
||||||
|
- 数据字典 DD0623:`dd_raster`(栅格/遥感影像,**本次新增**,展示模式 2D,形态=栅格)。
|
||||||
|
- 纳入 2D 过滤:`dimOf`/`dimensionOf` 增 `dd_raster` → `Dim2D`;但 col2D 勾选渲染须**按 ddCode 分派**——轨迹走 `loadMapLine`,栅格走**栅格加载**(不可让栅格走轨迹端点)。
|
||||||
|
- 渲染:取栅格的**地理范围(四至/仿射)+ 像素**,作为**地理配准的纹理平面贴到地形上**(带高程,可被 §5 高程拖动),类似底图瓦片按经纬度定位。
|
||||||
|
- 依赖:dd_raster 的**数据端点**(返回像素 + 四至/投影)——**待确认**,未明确前 §6 不落地(先做 §3–§5)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 维度过滤口径(对齐数据字典 DD0623,已部分落地)
|
||||||
|
- 2D(足迹/栅格):`dd_trajectory_data`(统一通用轨迹,"保留",已并入 dd_radar_rtk_trajectory)+ `dd_raster`(本期新增,随 §6)。
|
||||||
|
- 已删除、不再单列:`dd_radar_rtk_trajectory` / `dd_transient_electromagnetic_trajectory_data` / `dd_radar_channel_trajectory`(字典均"删除")。已清理:commit c1a824e。
|
||||||
|
- `dd_radar_2d` / `dd_radar_3d`:字典为 `展示模式=3D`(通道剖面 / 三维插值模型)→ **属三维分析,不进 2D 过滤**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 与雷达客户反馈的边界(本 spec **不含**)
|
||||||
|
- 雷达 TM 模型(单/双/多频,每频一个 `dd_radar_2d`/`dd_radar_3d`,共用一个 `dd_trajectory_data` 轨迹)→ 数据模型,与本 spec 无冲突。
|
||||||
|
- 雷达**数据在 3D 视图的渲染**(二维雷达=线/curtain、三维雷达=切面,按 trace 坐标,带打标 hover tip)→ **三维分析的另立任务**。
|
||||||
|
- 详情页用 trace 坐标校准异常 + 剖面打标 → 详情页另立任务。
|
||||||
|
- **2D 视图只显示轨迹线、打标暂不在 2D 展示**(客户 #6 修正)→ 与本 spec 的"2D 显示轨迹足迹"一致,无新增 2D 工作。
|
||||||
|
- 雷达轨迹就是 `dd_trajectory_data`,本 spec 的 2D 分析按统一轨迹处理,无需特判。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 实现分期
|
||||||
|
- **A. 一场景两相机**:切 tab → 锁定近俯视/恢复自由相机 + 翻另一方数据集可见标志(§3、§4)。基础,先做。
|
||||||
|
- **B. 高程拖动**:2D 拾取单/多选 + 仅 Z 拖动 + 锁 XY + 实时读数(§5)。
|
||||||
|
- **C. dd_raster**:过滤纳入 + 按 ddCode 分派渲染 + 栅格地理配准贴地形(§6)。依赖栅格数据端点确认。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 风险 / 待定
|
||||||
|
- 近俯视角度(75–80°)需实机调;用户若坚持绝对正俯视,则 §5 高程反馈改为纯数值(不直观)。
|
||||||
|
- §4 翻可见标志需可靠区分每个 actor 的维度归属(按数据集 ddCode 记录维度 → actor)。
|
||||||
|
- §5 高程是否持久化/落库待定(暂会话内)。
|
||||||
|
- §6 dd_raster 数据端点(像素 + 四至/投影)未确认 → C 期阻塞点。
|
||||||
|
- 切相机/可见标志切换需与现有 `view2DModeChanged`、底图、地形 VE(垂直夸张)逻辑兼容,勿互相打架。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 验收
|
||||||
|
- 切到二维分析:相机变近俯视、**不能旋转**,只能平移+缩放;3D 数据集(体/切片/剖面)不可见,轨迹+地形+底图可见。
|
||||||
|
- 切回三维分析:恢复自由相机;3D 数据集重新可见,2D 轨迹隐藏。切换**瞬时无卡顿**(无重建)。
|
||||||
|
- 二维分析里选中一条/多条轨迹,竖向拖动→只改高程、地理位置不动、实时显示高程;叠在一起的层能被拉开。
|
||||||
|
- (C 期)勾选 dd_raster → 栅格按地理范围贴在地形上、可被高程拖动;轨迹与栅格各走各的加载路径、互不串。
|
||||||
|
- 维度过滤与数据字典 DD0623 一致(2D=trajectory_data+raster;radar_2d/3d 归 3D)。
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
# GPR 多通道三维体渲染性能问题 — 分析文档(供外部专家评审)
|
||||||
|
|
||||||
|
> 自包含技术文档。读者无需了解本代码库内部,只需具备 GPU 体绘制 / VTK 基础。
|
||||||
|
> 目的:把"探地雷达(GPR)多通道阵列数据渲成可交互三维体"遇到的性能问题、已试方案、实测数据、
|
||||||
|
> 待定关键点完整呈现,供外部专家判断方向。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与系统
|
||||||
|
|
||||||
|
- 桌面端 C++ 应用(Qt6 + **VTK 9.6.2**),渲染探地雷达(GPR)采集的地下三维数据,要求**可交互**(旋转/缩放)。
|
||||||
|
- 渲染用 VTK 的 `vtkGPUVolumeRayCastMapper`(OpenGL GPU 光线投射体绘制)。
|
||||||
|
- 当前测试机 GPU:32 个着色器纹理单元(典型独显/中端)。
|
||||||
|
|
||||||
|
## 2. 数据特征(关键,决定一切)
|
||||||
|
|
||||||
|
多通道阵列 GPR,一次采集一条"测线(line)":
|
||||||
|
- **道(trace)**:一个位置一根天线的垂直回波,深度方向 **821 采样**。
|
||||||
|
- **通道(channel)**:阵列横向并排的多对天线,本数据 **14 通道**;**相邻通道横向间距 ≈ 10.5cm**(来自 `.ord` 文件真实偏移 -0.686…+0.686m,跨度 1.37m)。
|
||||||
|
- **沿测线道间距 ≈ 4.9cm**(比横向通道间距细 ~2 倍)。
|
||||||
|
- 一条测线:沿路 **~45305 道**,覆盖 **~2.2km**(一条南北向道路)。
|
||||||
|
- **共 20 条测线 = 同一条路来回扫 20 趟**(车载,每趟阵列覆盖约 1.4m 宽,多趟铺横向)。
|
||||||
|
|
||||||
|
**单条线 = 一个三维体**:X=沿测线(~45305)、Y=通道(14)、Z=深度(821)。
|
||||||
|
**关键业务约束(来自现场专家)**:
|
||||||
|
- 通道太稀(10.5cm)→ 需**线内通道间插值**加密(相邻真实天线之间插,物理成立);
|
||||||
|
- **绝不做"测线之间"的插值**(车与车之间是真实物理空隙,插出来"信号全是假的",工程上不可接受);
|
||||||
|
- 多条测线"分开各自插值,渲染可以合到一起"。
|
||||||
|
|
||||||
|
GPR 数据的统计特征(实测,见 §6):**~91% 体素近零(反射层之间是空的),但反射层横贯整个深度分布**(不是集中一坨)。
|
||||||
|
|
||||||
|
## 3. 已建成并验证可用的功能(不是问题所在)
|
||||||
|
|
||||||
|
1. **线内通道插值**:读 `.ord` 真实横向偏移,规则化到 2.5cm 网格、相邻通道线性插值(不跨线)。
|
||||||
|
实测 Y 由 14 加密到 **56**。有单元测试。
|
||||||
|
2. **多体单遍合成**:20 条独立体(各自插值)作为一个 `vtkGPUVolumeRayCastMapper` 的多个端口注册进
|
||||||
|
`vtkMultiVolume`,**单遍 ray-cast 合成**(而非每条体一遍)。已验证。
|
||||||
|
3. **纹理单元上限自动退避**:单个 multi-volume 同时挂的体数受 GPU 每着色器纹理单元上限制约
|
||||||
|
(每体约吃 4 个单元 → 32 单元机上**一个包最多 7 体**,第 8 体报错并丢体)。已实现"渲一帧→报错则
|
||||||
|
每包减 1 重建重渲"的自动退避(强制 K=12 → 自动退避到 7,无丢体)。
|
||||||
|
4. **运行时换贴图边界**(确定性测试结论):给某端口**就地换贴图**——若**保持包围盒不变**(同范围、
|
||||||
|
只改 Y 密度)则 multi-volume 算得对;若**改包围盒**(任意子区域、origin/范围/spacing 变)则破坏
|
||||||
|
其缓存 `TexToBBox` → 体断开/消失。
|
||||||
|
5. 通道维 LOD、统一传函、色标图例等。
|
||||||
|
|
||||||
|
## 4. 核心问题:性能
|
||||||
|
|
||||||
|
- **20 条密体(Y=56)总览,交互极卡**:静止 ~1.7 fps,旋转/缩放掉到 < 1 fps(GPU 100%)。
|
||||||
|
- 渲染**视觉正确**(雷达剖面纹理清晰、横向连续、合成无误),纯属性能。
|
||||||
|
- 现有提速手段都是**"交互时降质"**方向(降屏幕分辨率、加粗采样步长)——**损可见质量**,治标。
|
||||||
|
用户明确要求:"有没有不损可见质量的根本性提速(业界最佳实践)?"
|
||||||
|
|
||||||
|
## 5. 已分析/已试的所有方案(含理由与状态)
|
||||||
|
|
||||||
|
| # | 方案 | 理由 | 状态 / 结论 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A | multi-volume 单遍合成 | 把"N 体=N 遍 ray-cast"降到分包遍数 | ✅ 已实现。但单遍内每步仍要在重叠体里逐个采样 |
|
||||||
|
| B | 纹理单元自动退避分包 | 绕开 GPU 纹理单元上限、不丢体 | ✅ 已实现(K=7/包,20 条=3 包)。代价:**跨包重叠合成不正确(接缝)** |
|
||||||
|
| C | 交互降屏幕采样(ImageSampleDistance) | VTK AutoAdjust 标准手段 | ✅ 已做。**损质量**;且 AutoAdjust 只降屏幕、不降沿光线步长 |
|
||||||
|
| D | 交互手动加粗沿光线步长(SampleDistance) | 通道插值后 Y 密→自动步长极细→巨卡;这才是大头 | ✅ 已实现(`--sampleDist`/`--dragSampleMul`)。**损质量;且用户尚未实测**(见 §7 待定) |
|
||||||
|
| E | 通道维 LOD(远疏近密换 Y 平面子集)| 保包围盒换贴图(#4 验证安全) | ✅ 已实现。但**只减纹理内存、不减每步重叠体采样次数 → 对此瓶颈几乎无效** |
|
||||||
|
| F | **装箱单体(binning)**:各线先逐线插值,再把**真实道**摆进一个总览网格体(空隙透明、不跨线插值)| 一个体一遍、每步采 1 次 → ~20×;真实数据无假信号 | ⚠️ 技术可行、合规,但**用户否决**:装箱合并后**总览里分不出各线**,而用户要"一起渲染时仍能逐线区分/查看"→ 合并即失去意义 |
|
||||||
|
| G | **空体素跳过 ESS(换 OSPRay/ANARI 后端)**:跳过透明背景块 | 业界对稀疏体的头号提速、不损质量 | ❌ **实测对本数据收益有限**(见 §6):保质量阈值下仅 ~2×,且**ESS 跳的是空区、不解决"重叠"**。VTK 库存 mapper 无自动 ESS;OSPRay 本环境未编、vcpkg 无包 |
|
||||||
|
| H | 减少同屏体数(只渲选中 ≤7 条)| 真实工作流本就是选几条,1 包 1 遍 | ✅ 免费、永远有效(使用方式,非技术) |
|
||||||
|
|
||||||
|
## 6. 关键实测数据
|
||||||
|
|
||||||
|
### 6.1 ESS(空体素跳过)潜力——零依赖实测,决定要不要上 OSPRay
|
||||||
|
对一条真实测线密体(5702×56×789),按块算 min/max,统计"整块落在近零透明带"的占比(=ESS 可跳块):
|
||||||
|
|
||||||
|
| 透明带半宽(相对半值域) | 8³ 块可跳占比 | 理论提速上限 1/(1−占比) | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 5% | 8% | 1.1× | 极保守 |
|
||||||
|
| **10%(保质量,不丢弱反射)** | **52%** | **~2.1×** | — |
|
||||||
|
| 20% | 80% | ~4.9× | 开始把弱反射当背景丢 |
|
||||||
|
| 30%(激进)| 90% | ~10× | 明显损质量 |
|
||||||
|
|
||||||
|
- 体素层面 **91% 近零**,但块层面(ESS 实际粒度)保质量阈值下**只能跳 ~52% → 理论 ~2×**。
|
||||||
|
- 原因:**反射层横贯整个深度分布**,多数块里总混着信号、跳不掉。要 10× 须用激进阈值(损质量)。
|
||||||
|
- **更关键**:ESS 跳"空区",**不减少"重叠"**——在有信号的块里仍要逐个采样所有重叠体。
|
||||||
|
|
||||||
|
### 6.2 其它实测
|
||||||
|
- multi-volume 纹理单元上限:本机 7 体/包(32 单元),第 8 体报 "Hardware does not support the number of textures"。
|
||||||
|
- 体维度示例:coarse 4 → ~11000×56×793/线;coarse 32 → ~1400×56×780/线。
|
||||||
|
- 全 20 条 dense(coarse 32):底图 level 0、Y=56、分 3 包,**渲染正确**;交互 ~1.7fps(**未加 D 方案的旧构建**)。
|
||||||
|
|
||||||
|
## 7. 关键点——已实测,结论修正(原假设两条都被推翻)
|
||||||
|
|
||||||
|
### 7.1 "重叠几层"——实测:**平均 ~8.7 层、最大 15 层(不是 ~2–3,也不是 20)**
|
||||||
|
纯几何测(各线世界 AABB 投到 X-Y 俯视 footprint、细网格统计每格覆盖层数,`--overlapStat`):
|
||||||
|
- footprint:横向 X≈37m、沿路 Y≈2.2km;
|
||||||
|
- **有体覆盖处平均重叠 8.74 层,最大 15 层**;穿 12–14 个体的格子占 ~42%。
|
||||||
|
- **结论修正**:原"~2–3 层"假设**错**(开发团队和外部专家都猜偏了);**重叠是真实的大瓶颈**。
|
||||||
|
- **且这 9 层是冗余**:20 趟是同一条路反复扫,同一地下点被测了 ~9 次 → 这 9 个重叠体在该处**都非空**
|
||||||
|
(同一地下结构)→ **ESS 在重叠区一个都跳不掉**(再次印证 ESS 不解决重叠)。
|
||||||
|
|
||||||
|
### 7.2 "采样 vs 重叠谁是大头"——实测:**采样瓶颈,fps 线性正比于步长**
|
||||||
|
20 条密体、静止近景、离屏(步长越大越快越糙):
|
||||||
|
|
||||||
|
| sampleDist | fps | 相对 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.2(≈自动细)| 1.3 | 1× |
|
||||||
|
| 0.5 | 3.2 | 2.5× |
|
||||||
|
| 1.0 | 5.9 | 4.5× |
|
||||||
|
| 2.0 | 11.3 | 8.7× |
|
||||||
|
| 4.0 | 20.9 | 16× |
|
||||||
|
|
||||||
|
- **步长翻倍→fps 翻倍 → GPU 是采样瓶颈**。总开销 ≈ 光线数 × (光线长/步长) × ~9 个重叠体。
|
||||||
|
- D 方案(手动步长)**确实直接、强力提速**;但**保质量的步长(≈ Nyquist,0.5×体素)下仍只 ~2 fps**
|
||||||
|
——因为 **9× 冗余重叠**把它乘了回去。要到交互级(10+fps) 得把步长粗到 ~2.0(欠采样、损 Z 薄层)。
|
||||||
|
|
||||||
|
### 7.3 合并诊断(两测合起来)
|
||||||
|
**慢 = 采样密度 × ~9 倍冗余重叠,两者都真实。**
|
||||||
|
- D 方案(粗化采样):提速强,但保质量步长下被 9× 重叠压回 ~2fps;要交互须损质量。
|
||||||
|
- **唯一"保质量又快"的,是去掉那 9× 冗余重叠**(同路重扫的同一地下点):合并/装箱(取真实道、不跨线
|
||||||
|
插值)→ 一个体一遍 → ~9× 提速、且 Nyquist 步长下也能交互、**零质量损失**(冗余测量本就该合并降噪)。
|
||||||
|
- **但这与用户"保持 20 条可区分"直接冲突**——而这 9 层在物理上是**冗余测量**(同一地下结构扫了 9 遍),
|
||||||
|
保持它们"可区分"的工程价值存疑。
|
||||||
|
|
||||||
|
### 7.4 CPU OSPRay vs GPU(仍未测)
|
||||||
|
ESS 对本数据 ~2× 且不解决 9× 重叠;OSPRay 主要 CPU、对手是 GPU 数千核。**很可能换 OSPRay 比现状还慢**,
|
||||||
|
且为 ~2× 重编整个 VTK 投入产出极差。不建议在去掉冗余重叠之前考虑。
|
||||||
|
|
||||||
|
### 7.5 "多线为何卡"的根因确诊(passcost,决定架构)——结论:**不是固定开销,是没用 LOD**
|
||||||
|
> 背景:最初 P11/P12 是"各线独立 mapper + 视野 LOD",实测仍 0.5fps。需确诊卡在三个嫌疑哪个:
|
||||||
|
> ①LOD 选区没削小 ②N 遍固定开销 ③重叠没摊掉。`passcost` 命令:N 个独立 GPU mapper 各渲一个 64³ 小体
|
||||||
|
> (模拟 LOD 削过的小区),分"铺开/不重叠"与"叠在一起/重叠",测离屏稳态 fps vs N。
|
||||||
|
|
||||||
|
| N | 铺开(不重叠) fps | 叠加(重叠) fps |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 177 | 204 |
|
||||||
|
| 5 | 162 | 43 |
|
||||||
|
| 10 | 144 | 22 |
|
||||||
|
| **20** | **78** | **11** |
|
||||||
|
|
||||||
|
**判读(决定性):**
|
||||||
|
- **嫌疑 ②(N 遍固定开销)排除**:20 个独立 mapper 铺开仍 **78fps**(177→78,远非线性)。
|
||||||
|
→ **各线独立 mapper 架构上完全可行,固定开销温和。"multi-volume 单遍 ⊥ 视野 LOD"这个不可兼得不致命**——
|
||||||
|
放弃单遍、回独立 mapper 并不慢。
|
||||||
|
- **嫌疑 ③(重叠)真实但小体下可控**:叠加随 N ~1/N(每条光线乘 N),但 **20 层 64³ 叠加仍 11fps**(可用),
|
||||||
|
再叠屏幕降采样更快。
|
||||||
|
- **嫌疑 ①(选区没削小)= 真凶**:passcost 小体 20 层叠加=11fps,而真实 view-all 只 1.7fps——差距全在
|
||||||
|
**贴图大小**:当前渲的是**整卷底图**(~11000×56×200 ≈ 上亿体素/条),**根本没用视野 LOD 把它削成小区**。
|
||||||
|
这是**最好结局:可修,不动地基,只需真正用上 LOD**。
|
||||||
|
|
||||||
|
**对架构的直接含义**:本会话引入的 **multi-volume 单遍是错误取舍**——为"单遍"关掉了 LOD、改固定整卷贴图,
|
||||||
|
导致大贴图 × 9 层重叠 = 1.7fps。而 passcost 证明独立 mapper 够快,**根本不必为单遍牺牲 LOD**。
|
||||||
|
|
||||||
|
## 8. 部署约束(硬件不确定,跨厂商)
|
||||||
|
|
||||||
|
客户机配置未知(可能无独显,或 N卡/A卡/Intel)。**没有任何单一渲染器能在 N 卡和 A 卡上都做"GPU+ESS"**——
|
||||||
|
GPU 体光追渲染器全厂商锁定(N 卡→NVIDIA VisRTX/OptiX/IndeX;Intel→OSPRay-GPU;A 卡→基本无成熟方案)。
|
||||||
|
跨厂商唯一通用的是 **OSPRay-CPU(免显卡、任意 x86)** 或 **OpenGL(任意 GPU、但无 ESS=现状)**。
|
||||||
|
若上多后端,需"OSPRay-CPU 保底 + 探测到 N卡/Intel独显时升对应 GPU 后端 + OpenGL 终极兜底"。
|
||||||
|
|
||||||
|
## 9. 关键问题——大多已被实测回答
|
||||||
|
|
||||||
|
1. ~~有没有不损质量的根本性提速法~~ **有,且是通用解:LOD(视野自适应多分辨率)**——让 GPU 单帧实际
|
||||||
|
ray-cast 的体素量**与数据总量解耦、只与"屏幕能看清的量"挂钩**(Task 12c 单体已验证 752/380fps)。
|
||||||
|
§7.5 passcost 证明它**对多线也成立**(独立 mapper 开销温和,20 条铺开 78fps)。
|
||||||
|
2. ~~"卡"的主因~~ **已确诊(§7.5):不是 N 遍固定开销(排除),是【当前根本没用 LOD、渲整卷大贴图】(嫌疑①)。**
|
||||||
|
本会话引入的 multi-volume 单遍为"单遍"关掉了 LOD → 大贴图 × 9 层重叠 → 1.7fps。
|
||||||
|
3. **9 层重叠的正确定位**(外部专家纠偏 + passcost 印证):它只是**这批数据(同路重扫 20 趟)的特例倍数**,
|
||||||
|
**不是渲染本质问题**。本质是"单帧采样量 > GPU 吞吐",通用解是 LOD(扛任意大数据:无重叠但更大也能扛)。
|
||||||
|
9 层重叠在 LOD 之上降级为"一个被摊薄的常数因子"(passcost:20 层小体叠加仍 11fps)。
|
||||||
|
**不要让渲染架构围绕这个特例设计。**
|
||||||
|
4. ESS/OSPRay/多后端:**继续埋掉**——ESS 对本数据 ~2× 且不解决重叠、CPU 对手是 GPU,且**它解决的是 LOD
|
||||||
|
已经解决的通用问题**,投入产出差。
|
||||||
|
|
||||||
|
## 10. 最终结论(passcost 确诊后,架构清晰)
|
||||||
|
- **渲染架构 = LOD 中心(视野自适应、单帧量与总量解耦)。** 这是扛"任意大数据"的通用根本解,
|
||||||
|
Task 12c 单体已验证、§7.5 passcost 多线也成立。
|
||||||
|
- **本会话的 multi-volume 单遍是错误取舍**:为"单遍合成"牺牲了 LOD、改固定整卷大贴图,正是当前 1.7fps 的
|
||||||
|
直接原因。passcost 证明独立 mapper 开销温和(20 条 78fps)→ **根本不必为单遍弃 LOD**。
|
||||||
|
- **正解 = 各线独立 mapper + 视野 LOD(逐线用 Task 12c 引擎)+ 停手才重建**(不每帧重建,避免 P11/P12
|
||||||
|
那种"20 条每帧重建上传"的 thrash——那才是 0.5fps 的另一半原因,与稳态 ray-cast 无关,已被 P13 思路解决)。
|
||||||
|
让每条线只渲视野内小区 → 即使 9 层叠加也可用。
|
||||||
|
- **9 层重叠 = LOD 之上的可选应用层优化**(对同路重扫冗余可"合并/降噪",顺带省 9×),**不进渲染地基**。
|
||||||
|
用户要逐条区分就不合并(靠 LOD 摊薄),要纯总览就合并。
|
||||||
|
- **采样步长(D 方案)= LOD 框架内的质量旋钮**,非独立根本解。
|
||||||
|
- **ESS/OSPRay/多后端:不做**(不解决 LOD 已解决的通用问题,对本数据收益差)。
|
||||||
|
|
||||||
|
→ **下一步(确诊已完成,可开工)**:把多线总览从"multi-volume 单遍固定整卷"改回"各线独立 mapper +
|
||||||
|
视野 LOD + 停手重建",让单帧渲染量随视野走、与 20 条总量解耦;实测多线总览是否达交互级。
|
||||||
|
这是顺着通用 LOD 框架、被 passcost 数据支撑的明确方向——不再围着 9 层重叠这个特例转。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 附:相关已落地代码 / 诊断工具(如专家要复现)
|
||||||
|
- 通道插值:`src/io/gpr/GprGeometry.cpp::planChannelInterpolation` + `Gpr3dvVolumeBridge.cpp`
|
||||||
|
- 多体合成/退避/质量控制/通道 LOD:`tools/gpr_poc/main.cpp::cmdViewAll`
|
||||||
|
- **诊断命令**(`tools/gpr_poc/main.cpp`,可直接跑复现 §6/§7 的数):
|
||||||
|
- `gpr_poc ess-stat <dir> <line>`:ESS 空块潜力(§6.1)
|
||||||
|
- `gpr_poc view-all <dir> <gps> --overlapStat`:实测重叠层数(§7.1)
|
||||||
|
- `gpr_poc view-all <dir> <gps> --sampleDist D`:步长↔fps(§7.2)
|
||||||
|
- `gpr_poc passcost --size 64 --overlap 0|1`:N 遍开销 vs 重叠 隔离测(§7.5)
|
||||||
|
- 数据/插值口径 spec:`docs/superpowers/specs/2026-06-25-gpr-line-channel-interpolation-and-multivolume.md`
|
||||||
|
- 多后端 ESS 架构 spec(**结论:不做,见本文 §10**):`docs/superpowers/specs/2026-06-26-gpr-multibackend-ess-rendering.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 摘要(一页结论,供决策)
|
||||||
|
1. **现象**:20 条通道插值密体总览,~1.7fps、交互更卡。视觉正确,纯性能。
|
||||||
|
2. **确诊**(passcost 隔离测):**不是 N 遍固定开销**(20 独立 mapper 铺开 78fps,排除);
|
||||||
|
是**当前根本没用视野 LOD、在渲整卷大贴图**(× 9 层重叠)。本会话的 multi-volume 单遍为"单遍"
|
||||||
|
牺牲了 LOD,是直接原因。
|
||||||
|
3. **通用根本解 = LOD**(单帧渲染量与数据总量解耦),扛任意大数据;Task 12c 单体 752fps、passcost 多线
|
||||||
|
也成立。9 层重叠只是**本批数据的特例倍数**,是 LOD 之上一个可摊薄/可选合并的因子,**不是架构核心**。
|
||||||
|
4. **正解**:各线独立 mapper + 视野 LOD + 停手才重建(弃 multi-volume 单遍)。
|
||||||
|
5. **明确否定**:ESS/OSPRay/多后端(对本数据 ~2×、不解决重叠、解决的是 LOD 已解决的通用问题)。
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
# ⚠️ 本 spec 已被实测推翻,勿照此实现
|
||||||
|
|
||||||
|
> **结论(见 `2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md` §10):ESS/OSPRay/多后端【不做】。**
|
||||||
|
> 实测:ESS 对本数据 ~2× 且不解决重叠;passcost 确诊"多线卡"的真因是【没用视野 LOD、渲整卷大贴图】,
|
||||||
|
> 不是固定开销。**正解 = LOD 中心(各线独立 mapper + 视野 LOD + 停手重建),见下方实现计划。**
|
||||||
|
> 本文余下"多后端/ESS"内容仅作历史记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实现计划(LOD 中心多线总览 — 已确诊、可执行)
|
||||||
|
|
||||||
|
**目标**:把 `cmdViewAll` 从"multi-volume 单遍 + 固定整卷大贴图"改为"各线独立 mapper + 视野 LOD +
|
||||||
|
停手才重建",使单帧渲染量随视野走、与 20 条总量解耦。
|
||||||
|
|
||||||
|
**改动步骤(`tools/gpr_poc/main.cpp::cmdViewAll`)**:
|
||||||
|
1. `PlacedSource` 加回**每线自己的 `vtkSmartVolumeMapper`**(GPU 模式);删 multi-volume 用法
|
||||||
|
(multiMapper/multiVol/port 不再需要,但可暂留不碍)。
|
||||||
|
2. **装配**:删 `buildBundles` + 退避(无 multi-volume 即无纹理单元上限);改为逐线
|
||||||
|
`mapper->SetInputData(baseImage)` → `volume->SetMapper(mapper)` → `ren->AddVolume(volume)`,
|
||||||
|
各线 mapper 收进 `mappers` 向量(供质量控制)。
|
||||||
|
3. **开 LOD**:`gViewAllBaseOnly = false`(启用引擎选区换图);引擎换的是"改包围盒的子区域",
|
||||||
|
各线独立 mapper 下**安全**(无 multi-volume 可破坏)。
|
||||||
|
4. **关 channel LOD**:`gChanLod = false`——引擎金字塔已逐级降 Y,无需单独抽 Y 平面。
|
||||||
|
5. `viewAllPickOneLine`:`ps.mapper->SetInputData(ps.currentImg); ps.mapper->Update();`(非 multiMapper 端口)。
|
||||||
|
6. **停手才重建(已有,确认接线)**:拖动中(`viewAllOnInteracting`)只降质+重置裁剪、**不提交引擎目标**;
|
||||||
|
松手(`viewAllOnInteract`)/定时器 idle 才 `viewAllSubmitTargets`(提交 LOD 目标)+ `viewAllPickOneLine`
|
||||||
|
(拉就绪区域换上)。避免 P11/P12"每帧 20 条重建上传"thrash。
|
||||||
|
7. **质量旋钮保留**:`--sampleDist`/`--maxImgSample`/`--dragSampleMul` 作 LOD 框架内的交互降质兜底。
|
||||||
|
8. **总览级别**:引擎 `selectLod` 按屏幕像素选层——拉远时全路映射到少量像素 → 自动选粗层(小贴图)→
|
||||||
|
20 条小贴图即可用;拉近 → 小区域细层。**确认 selectLod 多线下确实选到粗层**(若没有,调
|
||||||
|
selectLod 的屏幕像素阈值——这是 §7.5 嫌疑①的修复点)。
|
||||||
|
|
||||||
|
**验收**:离屏看各线底图 level 随相机距离变(远→粗/小、近→细/小区);真窗口测 20 条总览交互级 fps、
|
||||||
|
拖动跟手、松手清晰、过档位无明显卡顿(外部专家提示重点盯"停手重建过渡手感")。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# GPR 三维体渲染:多后端 + 空体素跳过(ESS) 加速架构 — Spec(2026-06-26,已废)
|
||||||
|
|
||||||
|
> 解决"20 个重叠密体在 VTK 库存 OpenGL mapper 上又卡又只能降质"的根本性方案。
|
||||||
|
> 关联:`docs/superpowers/specs/2026-06-25-gpr-line-channel-interpolation-and-multivolume.md`(数据/插值口径)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 一句话目标
|
||||||
|
用**空体素跳过(ESS)** 这一不损可见质量的业界技术,把"逐线分开的多个密体合并渲染"做到**不卡**;
|
||||||
|
并以**多渲染后端 + 自动适配**覆盖客户侧未知/多厂商硬件(含无显卡)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题与根因
|
||||||
|
- 现状渲染 = VTK `vtkGPUVolumeRayCastMapper`(OpenGL)。**任意 GPU 可跑(最通用),但无 ESS**。
|
||||||
|
- 20 个重叠半透明密体:每条光线每步在 20 体各采一次、光线又长 → 几十亿次纹理查找/帧 → 1.7fps、交互更卡。
|
||||||
|
- 通道插值后 Y 加密到 2.5cm,使自动沿光线步长更细 → 更卡。
|
||||||
|
- **已尝试的"交互降质"(屏幕降采样 + 沿光线步长加粗)是治标**,损可见质量。
|
||||||
|
|
||||||
|
## 2. 根本方案:ESS(不损质量)
|
||||||
|
GPR 体 ~90% 是近零背景(反射层之间空)。ESS 用 min/max 加速结构**跳过"在传函里全透明"的块**,
|
||||||
|
对稀疏数据常 5–50× 提速、**零质量损失**。但 **VTK 库存 GPU mapper 不做自动 ESS**(仅有受限的
|
||||||
|
`UseDepthPass` 等高线跳过)。→ 真 ESS 必须**换专业体渲染后端**(其底层自带 ESS + 正确合成多重叠体)。
|
||||||
|
|
||||||
|
## 3. 关键事实:跨厂商 GPU 加速不存在单一方案
|
||||||
|
GPU 体光追渲染器全是**厂商锁定**:
|
||||||
|
|
||||||
|
| 后端 | 硬件 | ESS | 跨厂商 | 角色 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| OpenGL(vtkGPUVolumeRayCastMapper,现状)| 任意 GPU(N/A/Intel) | ❌ | ✅ | **终极兜底** |
|
||||||
|
| OSPRay(CPU,Embree/ISPC)| 任意 x86 CPU(免显卡)| ✅ | ✅ | **通用基线** |
|
||||||
|
| OSPRay-GPU(SYCL/oneAPI)| 仅 Intel Arc/数据中心卡 | ✅ | ❌ | Intel 独显 |
|
||||||
|
| ANARI + VisRTX(OptiX)| 仅 NVIDIA | ✅ | ❌ | N 卡 |
|
||||||
|
| AMD GPU 体渲染+ESS | — | — | ❌ 无成熟方案 | — |
|
||||||
|
|
||||||
|
**结论**:没有"同时 N/A 卡的 GPU-ESS"。**面向未知客户机,OSPRay-CPU 是最稳通用选择**(免显卡、ESS、质量不降)。
|
||||||
|
|
||||||
|
## 4. 渲染后端架构(多后端 + 自动适配 + 手动覆盖 + 兼容灰掉)
|
||||||
|
|
||||||
|
### 4.1 用户可见选项(按硬件/结果命名,不暴露库名)
|
||||||
|
| 用户看到 | 背后实现 | 适配硬件 |
|
||||||
|
|---|---|---|
|
||||||
|
| **自动(推荐)** | 探测后选下面之一 | — |
|
||||||
|
| GPU 加速(N卡)| ANARI + VisRTX | NVIDIA 独显 |
|
||||||
|
| GPU 加速(Intel)| OSPRay-GPU | Intel Arc 独显 |
|
||||||
|
| CPU(通用,免显卡)| OSPRay-CPU | 任意 CPU |
|
||||||
|
| 通用 GPU(兼容)| OpenGL(现状 mapper)| 任意 GPU(兜底)|
|
||||||
|
|
||||||
|
### 4.2 自动探测逻辑
|
||||||
|
```
|
||||||
|
if NVIDIA 独显: → VisRTX(GPU)
|
||||||
|
elif Intel Arc 独显: → OSPRay-GPU
|
||||||
|
elif AMD(独显/核显) 或 Intel 核显 或 无显卡 或 探测失败:
|
||||||
|
→ OSPRay-CPU(默认) // 核显一律走 CPU:弱+共享内存+多不被GPU后端支持
|
||||||
|
// 强力 A 卡可手动选"通用 OpenGL"用其 GPU(无 ESS),但不作默认
|
||||||
|
```
|
||||||
|
- **手动覆盖**:用户可自选,但**只列出与当前硬件兼容的项**(不兼容灰掉,如 A 卡上禁 VisRTX)。
|
||||||
|
- **集显建议**:一律 OSPRay-CPU(CPU+ESS 比让弱核显硬渲更稳更快)。
|
||||||
|
|
||||||
|
### 4.3 部署策略(一句话)
|
||||||
|
**OSPRay-CPU 保底通用;探测到 N卡/Intel Arc 时升对应 GPU 后端;A 卡/核显吃 CPU 基线;OpenGL 终极兜底。**
|
||||||
|
|
||||||
|
## 5. 上 ESS 后端后,废弃哪些
|
||||||
|
- ❌ **装箱单体(binning)**:当初为绕 OpenGL 无 ESS 才提(代价=丢"逐线分开")。ESS 后端让**逐线分开
|
||||||
|
的多体也快** → 不需要装箱。**逐线分开 + 不造假 + 不卡,三者兼得。**
|
||||||
|
- ❌ **交互降质权宜**(屏幕/沿光线步长加粗):ESS 后端有自带的自适应/渐进式细化,基本不用;保留作任何后端的兜底。
|
||||||
|
- ❌ 自写 ESS shader / 预积分 / UseDepthPass:后端自带,无需自行实现。
|
||||||
|
- ✅ **保留**:"选几条 ds(≤7)" 是使用方式(非技术),永远有效。
|
||||||
|
|
||||||
|
## 6. 实现要点(工程)
|
||||||
|
- VTK 需**重编**带:`RenderingRayTracing`(OSPRay)、`RenderingAnari`(ANARI/VisRTX) 模块 + 依赖
|
||||||
|
(OSPRay/Embree/ISPC/OpenVKL;ANARI-SDK/VisRTX)。用户已确认工程量无所谓。
|
||||||
|
- 渲染层抽象一个 `IVolumeRenderBackend`,运行时按 §4 选具体 mapper:
|
||||||
|
`vtkGPUVolumeRayCastMapper`(OpenGL) / `vtkOSPRayPass`+volume / `vtkAnariPass`+volume。
|
||||||
|
- **数据不变**:逐线密体(含通道插值,spec 前一份)原样喂各后端;多体合成由后端负责(无 K=7 分包)。
|
||||||
|
- 硬件探测:GL_VENDOR / 平台 API(DXGI 枚举适配器)判 N/A/Intel + 独显/核显。
|
||||||
|
|
||||||
|
## 7. POC 计划(先验"CPU+ESS 够不够快"——最通用、风险最低)
|
||||||
|
1. **POC-1(先做)**:最小程序,用 **OSPRay-CPU** 渲一个 GPR 密体(tmp/lines_all_dense 里一条/几条),
|
||||||
|
**实测普通 CPU 上对多体/密体的 fps + ESS 提速比**,对照现状 OpenGL。先确认 OSPRay 在本环境
|
||||||
|
可编可跑、CPU+ESS 实际够快——这是整套方案值不值得上的关键闸门。
|
||||||
|
2. **POC-2**:若有 N 卡,ANARI+VisRTX 渲同一体,对照 GPU 提速。
|
||||||
|
3. POC 通过 → 才动手重编 VTK + 接后端抽象层。
|
||||||
|
|
||||||
|
## 8. 风险 / 待定
|
||||||
|
- OSPRay-GPU(Intel)较新、不如 CPU 路成熟;ANARI/VisRTX 需 NVIDIA 驱动 + VisRTX 库。
|
||||||
|
- 各后端传函/外观与 OpenGL 有差异,需重新调一致。
|
||||||
|
- 本环境能否编出带光追/ANARI 的 VTK(vcpkg/手动依赖)待 POC-1 验证。
|
||||||
|
- CPU+ESS 在低核机上的实际帧率待实测。
|
||||||
|
|
||||||
|
## 9. 验收
|
||||||
|
1. 客户机无论有无显卡/何种显卡,自动选到可跑的后端并出图(OSPRay-CPU 永远兜底)。
|
||||||
|
2. 20 条密体总览:逐线分开、不造假、**ESS 后端下不卡**(目标交互 ≥ 可用帧率,质量不降)。
|
||||||
|
3. 手动选项只列兼容项;集显默认 CPU。
|
||||||
|
4. 数据层(通道插值密体)零改动复用。
|
||||||
|
|
@ -124,6 +124,16 @@ if (-not $SkipDeploy) {
|
||||||
} finally {
|
} finally {
|
||||||
if (-not $adsPreexisted) { Remove-Item $adsTmp -Force -ErrorAction SilentlyContinue }
|
if (-not $adsPreexisted) { Remove-Item $adsTmp -Force -ErrorAction SilentlyContinue }
|
||||||
}
|
}
|
||||||
|
# 中文化:windeployqt --no-translations 不带翻译,单独拷 Qt 自带 zh_CN(QMessageBox/QFileDialog
|
||||||
|
# 等标准按钮中文化;app 启动按 exe 旁 translations\ 加载)。
|
||||||
|
$qtZh = Join-Path $QtBin '..\translations\qtbase_zh_CN.qm'
|
||||||
|
if (Test-Path $qtZh) {
|
||||||
|
$stageTr = Join-Path $StageDir 'translations'
|
||||||
|
New-Item -ItemType Directory -Force $stageTr | Out-Null
|
||||||
|
Copy-Item $qtZh $stageTr -Force
|
||||||
|
} else {
|
||||||
|
Warn "未找到 qtbase_zh_CN.qm($qtZh)—部署版标准按钮可能仍为英文"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 5.5 随包数据:本地样本演示数据 + PROJ 数据(exe 旁布局,运行时相对定位)-------
|
# --- 5.5 随包数据:本地样本演示数据 + PROJ 数据(exe 旁布局,运行时相对定位)-------
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,28 @@
|
||||||
|
|
||||||
#include "EmptyAwareComboBox.hpp"
|
#include "EmptyAwareComboBox.hpp"
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPen>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPlainTextEdit>
|
#include <QPlainTextEdit>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "FormKit.hpp"
|
#include "FormKit.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
|
||||||
// 异常类型 mock 列表(label, id)。真实 exceptionType 端点只读、后续接。
|
|
||||||
struct TypeItem { const char* label; const char* id; };
|
|
||||||
const TypeItem kMockTypes[] = {
|
|
||||||
{"断层", "mock-fault"},
|
|
||||||
{"破碎带", "mock-fracture"},
|
|
||||||
{"含水构造", "mock-water"},
|
|
||||||
{"其它", "mock-other"},
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH,
|
AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH,
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo,
|
||||||
|
const QString& projectId, int remarkSourceType,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: QDialog(parent) {
|
: QDialog(parent), cmdRepo_(cmdRepo), remarkSourceType_(remarkSourceType) {
|
||||||
setWindowTitle(QStringLiteral("保存异常"));
|
setWindowTitle(QStringLiteral("保存异常"));
|
||||||
setModal(true);
|
setModal(true);
|
||||||
|
|
||||||
|
|
@ -42,10 +40,17 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i
|
||||||
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
|
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
|
||||||
|
|
||||||
type_ = new EmptyAwareComboBox();
|
type_ = new EmptyAwareComboBox();
|
||||||
for (const auto& t : kMockTypes)
|
type_->setPlaceholderText(QStringLiteral("请选择异常类型")); // 空(如该形态平台无类型)时显灰占位+「暂无数据」
|
||||||
type_->addItem(QString::fromUtf8(t.label), QString::fromUtf8(t.id));
|
|
||||||
formkit::capField(type_);
|
formkit::capField(type_);
|
||||||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_);
|
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_);
|
||||||
|
// 样式预览:选中类型的 legend 派生样式可视化(点=色球/线=线型/面=描边矩形)。
|
||||||
|
stylePreview_ = new QLabel(QStringLiteral("—"));
|
||||||
|
stylePreview_->setMinimumWidth(geopro::app::scaledPx(92));
|
||||||
|
form->addRow(formkit::editLabel(QStringLiteral("样式")), stylePreview_);
|
||||||
|
// 选中类型变化 → 拉其平台样式(legend),使保存的异常按平台类型样式渲染 + 刷新预览。
|
||||||
|
connect(type_, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||||
|
[this](int) { loadStyleForCurrent(); });
|
||||||
|
loadTypes(cmdRepo, projectId, remarkSourceType); // 异步拉平台异常类型填充(与平台一致)
|
||||||
|
|
||||||
remark_ = new QPlainTextEdit();
|
remark_ = new QPlainTextEdit();
|
||||||
remark_->setFixedHeight(geopro::app::scaledPx(60));
|
remark_->setFixedHeight(geopro::app::scaledPx(60));
|
||||||
|
|
@ -69,6 +74,78 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i
|
||||||
formkit::addDialogButtons(root, this);
|
formkit::addDialogButtons(root, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AnomalySaveDialog::loadTypes(geopro::data::IDatasetCommandRepository* cmdRepo,
|
||||||
|
const QString& projectId, int remarkSourceType) {
|
||||||
|
if (cmdRepo == nullptr || projectId.isEmpty()) return; // 无仓储/项目 → 下拉留空(空态提示)
|
||||||
|
QPointer<AnomalySaveDialog> self(this);
|
||||||
|
cmdRepo->listExceptionTypes(
|
||||||
|
projectId, QString::number(remarkSourceType),
|
||||||
|
[self](bool ok, QJsonArray list, const QString&) {
|
||||||
|
if (!self || !ok) return; // 对话框已关 / 失败 → 留空
|
||||||
|
for (const QJsonValue& v : list) {
|
||||||
|
const QJsonObject o = v.toObject();
|
||||||
|
// 平台响应项:{label:类型名, value:类型id}(实测扁平 data 数组,net 层已归一 value)。
|
||||||
|
self->type_->addItem(o.value(QStringLiteral("label")).toString(),
|
||||||
|
o.value(QStringLiteral("value")).toString());
|
||||||
|
}
|
||||||
|
self->loadStyleForCurrent(); // 首项自动选中 → 预取其样式
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalySaveDialog::loadStyleForCurrent() {
|
||||||
|
if (cmdRepo_ == nullptr) return;
|
||||||
|
const QString typeId = type_->currentData().toString();
|
||||||
|
if (typeId.isEmpty()) return;
|
||||||
|
QPointer<AnomalySaveDialog> self(this);
|
||||||
|
cmdRepo_->getExceptionTypeDetail(typeId, [self](bool ok, QJsonObject detail, const QString&) {
|
||||||
|
if (!self || !ok) return;
|
||||||
|
const QJsonObject lg = detail.value(QStringLiteral("legend")).toObject();
|
||||||
|
// 按形态(1点/2线/3面)从 legend 派生样式:点用 pointColor;线/面用 polyline*。
|
||||||
|
if (self->remarkSourceType_ == 1) {
|
||||||
|
self->styleColor_ = lg.value(QStringLiteral("pointColor")).toString();
|
||||||
|
} else {
|
||||||
|
self->styleColor_ = lg.value(QStringLiteral("polylineColor")).toString();
|
||||||
|
self->styleWidth_ = lg.value(QStringLiteral("polylineWidth")).toDouble();
|
||||||
|
self->styleDashed_ =
|
||||||
|
lg.value(QStringLiteral("polylineShape")).toString().contains(QStringLiteral("dash"));
|
||||||
|
}
|
||||||
|
self->updateStylePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalySaveDialog::updateStylePreview() {
|
||||||
|
if (stylePreview_ == nullptr) return;
|
||||||
|
const QColor col(styleColor_);
|
||||||
|
if (!col.isValid()) { // 未取到样式 → 占位
|
||||||
|
stylePreview_->setPixmap(QPixmap());
|
||||||
|
stylePreview_->setText(QStringLiteral("—"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int w = geopro::app::scaledPx(92), h = geopro::app::scaledPx(22);
|
||||||
|
QPixmap pm(w, h);
|
||||||
|
pm.fill(Qt::transparent);
|
||||||
|
QPainter p(&pm);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
QPen pen(col);
|
||||||
|
pen.setWidthF(std::clamp(styleWidth_ > 0.0 ? styleWidth_ : 2.0, 1.0, 4.0));
|
||||||
|
if (styleDashed_) pen.setStyle(Qt::DashLine);
|
||||||
|
if (remarkSourceType_ == 1) { // 点:实心色球
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(col);
|
||||||
|
p.drawEllipse(QPointF(w / 2.0, h / 2.0), h * 0.3, h * 0.3);
|
||||||
|
} else if (remarkSourceType_ == 2) { // 线:按线宽/虚实画线
|
||||||
|
p.setPen(pen);
|
||||||
|
p.drawLine(QPointF(w * 0.1, h / 2.0), QPointF(w * 0.9, h / 2.0));
|
||||||
|
} else { // 面:描边矩形 + 淡填充
|
||||||
|
p.setPen(pen);
|
||||||
|
p.setBrush(QColor(col.red(), col.green(), col.blue(), 40));
|
||||||
|
p.drawRect(QRectF(w * 0.12, h * 0.22, w * 0.76, h * 0.56));
|
||||||
|
}
|
||||||
|
p.end();
|
||||||
|
stylePreview_->setText(QString());
|
||||||
|
stylePreview_->setPixmap(pm);
|
||||||
|
}
|
||||||
|
|
||||||
QString AnomalySaveDialog::anomalyName() const {
|
QString AnomalySaveDialog::anomalyName() const {
|
||||||
const QString n = name_->text().trimmed();
|
const QString n = name_->text().trimmed();
|
||||||
return n.isEmpty() ? QStringLiteral("异常") : n;
|
return n.isEmpty() ? QStringLiteral("异常") : n;
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,52 @@ class QComboBox;
|
||||||
class QPlainTextEdit;
|
class QPlainTextEdit;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
class IDatasetCommandRepository;
|
||||||
|
}
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 异常保存对话框(#4b,需求 R50):名称 + 异常类型 + 备注 + 截图预览/大小。
|
// 异常保存对话框(#4b,需求 R50):名称 + 异常类型 + 备注 + 截图预览/大小。
|
||||||
// 异常类型本期 mock 列表(真实 exceptionType 端点只读、后续可接)。accept 后取 name/typeName/typeId/remark。
|
// 异常类型从平台按标注形态(remarkSourceType)异步拉取,与平台保持一致。accept 后取 name/typeName/typeId/remark。
|
||||||
class AnomalySaveDialog : public QDialog {
|
class AnomalySaveDialog : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
// screenshotPath:圈定结束截图的本地路径(为空则不显示预览);w/h:截图像素尺寸(R50「确定截图大小」)。
|
// screenshotPath:圈定结束截图的本地路径(为空则不显示预览);w/h:截图像素尺寸(R50「确定截图大小」)。
|
||||||
AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH, QWidget* parent = nullptr);
|
// cmdRepo/projectId:异步拉取平台异常类型填充下拉(与平台一致);remarkSourceType:标注形态 1点/2线/3面,
|
||||||
|
// 决定查询哪一类平台异常类型。cmdRepo 为空则下拉留空(空态由 EmptyAwareComboBox 提示)。
|
||||||
|
AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH,
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo, const QString& projectId,
|
||||||
|
int remarkSourceType, QWidget* parent = nullptr);
|
||||||
|
|
||||||
QString anomalyName() const;
|
QString anomalyName() const;
|
||||||
QString typeName() const;
|
QString typeName() const;
|
||||||
QString typeId() const;
|
QString typeId() const;
|
||||||
QString remark() const;
|
QString remark() const;
|
||||||
|
|
||||||
|
// 选中类型的平台样式(从 legend 按形态派生,与平台一致)。styleColor 空 = 未取到,调用方用默认样式。
|
||||||
|
QString styleColor() const { return styleColor_; }
|
||||||
|
double styleWidth() const { return styleWidth_; }
|
||||||
|
bool styleDashed() const { return styleDashed_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// 异步拉平台异常类型(label→显示, value→id)填充下拉;空/失败时下拉留空(EmptyAwareComboBox 提示)。
|
||||||
|
void loadTypes(geopro::data::IDatasetCommandRepository* cmdRepo, const QString& projectId,
|
||||||
|
int remarkSourceType);
|
||||||
|
// 拉当前选中类型的详情 legend → 按形态(点/线/面)派生 styleColor/Width/Dashed。
|
||||||
|
void loadStyleForCurrent();
|
||||||
|
// 据当前样式按形态画预览(点=色球/线=线型/面=描边矩形),画到 stylePreview_。
|
||||||
|
void updateStylePreview();
|
||||||
|
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
|
int remarkSourceType_ = 3;
|
||||||
|
QString styleColor_; // legend 派生线/点色(空=未取到)
|
||||||
|
double styleWidth_ = 0.0; // legend.polylineWidth
|
||||||
|
bool styleDashed_ = false; // legend.polylineShape 含 "dash"
|
||||||
|
|
||||||
QLineEdit* name_ = nullptr;
|
QLineEdit* name_ = nullptr;
|
||||||
QComboBox* type_ = nullptr;
|
QComboBox* type_ = nullptr;
|
||||||
|
QLabel* stylePreview_ = nullptr; // 选中类型样式预览(色块/线型)
|
||||||
QPlainTextEdit* remark_ = nullptr;
|
QPlainTextEdit* remark_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
|
||||||
setFrameShape(QFrame::StyledPanel);
|
setFrameShape(QFrame::StyledPanel);
|
||||||
applyTokenizedStyleSheet(
|
applyTokenizedStyleSheet(
|
||||||
this, QStringLiteral("QFrame{background:{{bg/panel}};border:1px solid {{border/default}};"
|
this, QStringLiteral("QFrame{background:{{bg/panel}};border:1px solid {{border/default}};"
|
||||||
"border-radius:10px;}"));
|
"border-radius:8px;}")); // radius/lg=8(规范§3.2 画布浮窗)
|
||||||
setFixedWidth(320);
|
setFixedWidth(320);
|
||||||
|
|
||||||
auto* v = new QVBoxLayout(this);
|
auto* v = new QVBoxLayout(this);
|
||||||
|
|
@ -63,9 +63,12 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
|
||||||
close->setFixedSize(24, 24);
|
close->setFixedSize(24, 24);
|
||||||
close->setCursor(Qt::PointingHandCursor);
|
close->setCursor(Qt::PointingHandCursor);
|
||||||
// 显式覆盖全局 QPushButton 的 padding(6px 14px)/border——否则 24×24 容不下 padding,× 被挤出不可见。
|
// 显式覆盖全局 QPushButton 的 padding(6px 14px)/border——否则 24×24 容不下 padding,× 被挤出不可见。
|
||||||
close->setStyleSheet(QStringLiteral(
|
// 颜色走 token(避免深色模式失效)。
|
||||||
"QPushButton{border:none;background:transparent;padding:0;margin:0;font-size:16px;color:#888;}"
|
applyTokenizedStyleSheet(
|
||||||
"QPushButton:hover{color:#2f6fed;}"));
|
close, QStringLiteral(
|
||||||
|
"QPushButton{border:none;background:transparent;padding:0;margin:0;font-size:16px;"
|
||||||
|
"color:{{text/secondary}};}"
|
||||||
|
"QPushButton:hover{color:{{accent/primary}};}"));
|
||||||
connect(close, &QPushButton::clicked, this, &AxesSettingsPanel::closed);
|
connect(close, &QPushButton::clicked, this, &AxesSettingsPanel::closed);
|
||||||
titleRow->addWidget(title);
|
titleRow->addWidget(title);
|
||||||
titleRow->addStretch(1);
|
titleRow->addStretch(1);
|
||||||
|
|
@ -100,7 +103,8 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
|
||||||
scaleSlider_->setSingleStep(1);
|
scaleSlider_->setSingleStep(1);
|
||||||
scaleSlider_->setPageStep(1); // 点击轨道按 1 步移动(默认 pageStep=10 → 点一下直接跳到 10 的 bug)
|
scaleSlider_->setPageStep(1); // 点击轨道按 1 步移动(默认 pageStep=10 → 点一下直接跳到 10 的 bug)
|
||||||
scaleLabel_ = new QLabel(QStringLiteral("1.0×"), this);
|
scaleLabel_ = new QLabel(QStringLiteral("1.0×"), this);
|
||||||
scaleLabel_->setStyleSheet(QStringLiteral("border:none;color:#888;min-width:36px;"));
|
applyTokenizedStyleSheet(scaleLabel_,
|
||||||
|
QStringLiteral("border:none;color:{{text/secondary}};min-width:36px;"));
|
||||||
connect(scaleSlider_, &QSlider::valueChanged, this,
|
connect(scaleSlider_, &QSlider::valueChanged, this,
|
||||||
[this](int v) { scaleLabel_->setText(QStringLiteral("%1.0×").arg(v)); });
|
[this](int v) { scaleLabel_->setText(QStringLiteral("%1.0×").arg(v)); });
|
||||||
scaleRow->addWidget(scaleSlider_, 1);
|
scaleRow->addWidget(scaleSlider_, 1);
|
||||||
|
|
@ -111,9 +115,7 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
|
||||||
auto* btns = new QHBoxLayout();
|
auto* btns = new QHBoxLayout();
|
||||||
auto* cancel = new QPushButton(QStringLiteral("取消"), this);
|
auto* cancel = new QPushButton(QStringLiteral("取消"), this);
|
||||||
auto* apply = new QPushButton(QStringLiteral("应用"), this);
|
auto* apply = new QPushButton(QStringLiteral("应用"), this);
|
||||||
apply->setStyleSheet(QStringLiteral(
|
apply->setDefault(true); // 主按钮:走全局 QPushButton:default 样式(accent/primary,随主题),不再硬编码蓝
|
||||||
"QPushButton{background:#2f6fed;color:white;border:none;border-radius:6px;padding:6px 18px;}"
|
|
||||||
"QPushButton:hover{background:#2a63d4;}"));
|
|
||||||
connect(cancel, &QPushButton::clicked, this, &AxesSettingsPanel::closed);
|
connect(cancel, &QPushButton::clicked, this, &AxesSettingsPanel::closed);
|
||||||
connect(apply, &QPushButton::clicked, this, [this] {
|
connect(apply, &QPushButton::clicked, this, [this] {
|
||||||
auto rd = [](const Row& r) {
|
auto rd = [](const Row& r) {
|
||||||
|
|
@ -147,7 +149,7 @@ AxesSettingsPanel::Row AxesSettingsPanel::addAxisRow(QVBoxLayout* col, const QSt
|
||||||
auto* range = new QHBoxLayout();
|
auto* range = new QHBoxLayout();
|
||||||
auto* loCol = new QVBoxLayout();
|
auto* loCol = new QVBoxLayout();
|
||||||
auto* loLbl = new QLabel(QStringLiteral("最小值"), this);
|
auto* loLbl = new QLabel(QStringLiteral("最小值"), this);
|
||||||
loLbl->setStyleSheet(QStringLiteral("border:none;color:#888;"));
|
applyTokenizedStyleSheet(loLbl, QStringLiteral("border:none;color:{{text/tertiary}};"));
|
||||||
loCol->addWidget(loLbl);
|
loCol->addWidget(loLbl);
|
||||||
r.lo = new QDoubleSpinBox(this);
|
r.lo = new QDoubleSpinBox(this);
|
||||||
r.lo->setRange(-1e6, 1e6);
|
r.lo->setRange(-1e6, 1e6);
|
||||||
|
|
@ -156,7 +158,7 @@ AxesSettingsPanel::Row AxesSettingsPanel::addAxisRow(QVBoxLayout* col, const QSt
|
||||||
range->addLayout(loCol);
|
range->addLayout(loCol);
|
||||||
auto* hiCol = new QVBoxLayout();
|
auto* hiCol = new QVBoxLayout();
|
||||||
auto* hiLbl = new QLabel(QStringLiteral("最大值"), this);
|
auto* hiLbl = new QLabel(QStringLiteral("最大值"), this);
|
||||||
hiLbl->setStyleSheet(QStringLiteral("border:none;color:#888;"));
|
applyTokenizedStyleSheet(hiLbl, QStringLiteral("border:none;color:{{text/tertiary}};"));
|
||||||
hiCol->addWidget(hiLbl);
|
hiCol->addWidget(hiLbl);
|
||||||
r.hi = new QDoubleSpinBox(this);
|
r.hi = new QDoubleSpinBox(this);
|
||||||
r.hi->setRange(-1e6, 1e6);
|
r.hi->setRange(-1e6, 1e6);
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/chart/BarChartView.cpp
|
panels/chart/BarChartView.cpp
|
||||||
panels/chart/LineChartView.cpp
|
panels/chart/LineChartView.cpp
|
||||||
panels/chart/TrajectoryMapView.cpp
|
panels/chart/TrajectoryMapView.cpp
|
||||||
|
panels/web/ProjectWebView.cpp
|
||||||
panels/chart/DetailViewFactory.cpp
|
panels/chart/DetailViewFactory.cpp
|
||||||
resources/map/map.qrc
|
resources/map/map.qrc
|
||||||
resources/keys.qrc
|
resources/keys.qrc
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,14 @@ EmptyAwareComboBox::EmptyAwareComboBox(QWidget* parent) : QComboBox(parent) {
|
||||||
|
|
||||||
int EmptyAwareComboBox::realItemCount() const {
|
int EmptyAwareComboBox::realItemCount() const {
|
||||||
int n = 0;
|
int n = 0;
|
||||||
|
auto* m = model();
|
||||||
for (int i = 0; i < count(); ++i) {
|
for (int i = 0; i < count(); ++i) {
|
||||||
// 排除临时「暂无数据」占位项。
|
// 排除临时「暂无数据」占位项。
|
||||||
if (itemData(i, kEmptyHintRole).toBool()) continue;
|
if (itemData(i, kEmptyHintRole).toBool()) continue;
|
||||||
// 排除不可选项(禁用 / NoItemFlags),它们不构成「真实可选数据」。
|
// 排除不可选项(禁用 / NoItemFlags)。用 model()->flags() 正确取项标志——
|
||||||
if (!(itemData(i, Qt::UserRole - 1).value<Qt::ItemFlags>() & Qt::ItemIsSelectable))
|
// 原 itemData(i, UserRole-1) 不是 Qt 的 flags 角色,对正常项恒返回不可选 →
|
||||||
continue;
|
// realItemCount 恒 0 → 有真实项也误插「暂无数据」(用户实测:异常区下方多一条暂无数据)。
|
||||||
|
if (m && !(m->flags(m->index(i, modelColumn())) & Qt::ItemIsSelectable)) continue;
|
||||||
++n;
|
++n;
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,11 @@ QVBoxLayout* dialogRoot(QDialog* dlg);
|
||||||
// 与「数据详情 / 属性面板」同款的卡片面。返回 QFrame;其 layout() 即 QVBoxLayout,向内 addSection/addLayout。
|
// 与「数据详情 / 属性面板」同款的卡片面。返回 QFrame;其 layout() 即 QVBoxLayout,向内 addSection/addLayout。
|
||||||
QFrame* formCard(QWidget* parent);
|
QFrame* formCard(QWidget* parent);
|
||||||
QVBoxLayout* cardBody(QFrame* card); // 取 formCard 的内层 QVBoxLayout(便捷器)
|
QVBoxLayout* cardBody(QFrame* card); // 取 formCard 的内层 QVBoxLayout(便捷器)
|
||||||
// 标准底部按钮栏:QDialogButtonBox(Ok|Cancel),已接 accept/reject;okText/cancelText 可定制文案。
|
// 标准底部按钮栏:QDialogButtonBox(Ok|Cancel),已接 accept/reject。
|
||||||
QDialogButtonBox* addDialogButtons(QVBoxLayout* root, QDialog* dlg, const QString& okText = QString(),
|
// 默认中文「确定/取消」(不依赖 Qt 翻译是否就位);调用方可覆盖(如「生成/取消」)。
|
||||||
const QString& cancelText = QString());
|
QDialogButtonBox* addDialogButtons(QVBoxLayout* root, QDialog* dlg,
|
||||||
|
const QString& okText = QStringLiteral("确定"),
|
||||||
|
const QString& cancelText = QStringLiteral("取消"));
|
||||||
|
|
||||||
// ── 可编辑表单:§7.0 统一度量(DynamicFormEditor 与各参数对话框共用,单一真相)──────
|
// ── 可编辑表单:§7.0 统一度量(DynamicFormEditor 与各参数对话框共用,单一真相)──────
|
||||||
QFormLayout* makeEditForm(); // 右对齐标签 + 标准行距/列距
|
QFormLayout* makeEditForm(); // 右对齐标签 + 标准行距/列距
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,27 @@
|
||||||
#include "SliceExport.hpp"
|
#include "SliceExport.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPainterPathStroker>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QPolygonF>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include <vtkCamera.h>
|
||||||
#include <vtkDataArray.h>
|
#include <vtkDataArray.h>
|
||||||
#include <vtkImageData.h>
|
#include <vtkImageData.h>
|
||||||
#include <vtkNew.h>
|
#include <vtkNew.h>
|
||||||
#include <vtkPNGWriter.h>
|
#include <vtkPNGWriter.h>
|
||||||
#include <vtkPointData.h>
|
#include <vtkPointData.h>
|
||||||
#include <vtkRenderWindow.h>
|
#include <vtkRenderWindow.h>
|
||||||
|
#include <vtkRenderer.h>
|
||||||
|
#include <vtkRendererCollection.h>
|
||||||
#include <vtkWindowToImageFilter.h>
|
#include <vtkWindowToImageFilter.h>
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -41,6 +55,178 @@ bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int&
|
||||||
return writer->GetErrorCode() == 0;
|
return writer->GetErrorCode() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor,
|
||||||
|
double minExtent, const std::string& path, int& outW, int& outH) {
|
||||||
|
outW = outH = 0;
|
||||||
|
if (win == nullptr || path.empty()) return false;
|
||||||
|
vtkRenderer* ren =
|
||||||
|
win->GetRenderers() ? win->GetRenderers()->GetFirstRenderer() : nullptr;
|
||||||
|
vtkCamera* cam = ren ? ren->GetActiveCamera() : nullptr;
|
||||||
|
if (ren == nullptr || cam == nullptr)
|
||||||
|
return captureRenderWindowPng(win, path, outW, outH); // 无渲染器 → 退回整窗
|
||||||
|
|
||||||
|
// 1) 区域包围盒:minExtent 兜底(点零体积/共面零厚度) → padFactor 以中心外扩留边距。
|
||||||
|
double b[6];
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
const double lo = regionBounds[2 * i], hi = regionBounds[2 * i + 1];
|
||||||
|
const double c = 0.5 * (lo + hi);
|
||||||
|
double half = 0.5 * (hi - lo);
|
||||||
|
if (2.0 * half < minExtent) half = 0.5 * minExtent; // 退化轴兜底
|
||||||
|
half *= padFactor; // 外扩边距
|
||||||
|
b[2 * i] = c - half;
|
||||||
|
b[2 * i + 1] = c + half;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 存相机现场(ResetCamera 改 position/focalPoint/clipping/parallelScale)。
|
||||||
|
double pos[3], fp[3], up[3], clip[2];
|
||||||
|
cam->GetPosition(pos);
|
||||||
|
cam->GetFocalPoint(fp);
|
||||||
|
cam->GetViewUp(up);
|
||||||
|
cam->GetClippingRange(clip);
|
||||||
|
const double va = cam->GetViewAngle();
|
||||||
|
const double ps = cam->GetParallelScale();
|
||||||
|
|
||||||
|
// 3) 重构图:保持视角方向,仅推近/缩放框住外扩区域。
|
||||||
|
ren->ResetCamera(b);
|
||||||
|
|
||||||
|
// 4) 截图(后台缓冲 + 关交换 → 屏幕不闪)。
|
||||||
|
vtkNew<vtkWindowToImageFilter> w2i;
|
||||||
|
w2i->SetInput(win);
|
||||||
|
w2i->ReadFrontBufferOff();
|
||||||
|
w2i->Update();
|
||||||
|
if (auto* img = w2i->GetOutput()) {
|
||||||
|
int dims[3];
|
||||||
|
img->GetDimensions(dims);
|
||||||
|
outW = dims[0];
|
||||||
|
outH = dims[1];
|
||||||
|
}
|
||||||
|
vtkNew<vtkPNGWriter> writer;
|
||||||
|
writer->SetFileName(path.c_str());
|
||||||
|
writer->SetInputConnection(w2i->GetOutputPort());
|
||||||
|
writer->Write();
|
||||||
|
const bool ok = writer->GetErrorCode() == 0;
|
||||||
|
|
||||||
|
// 5) 还原相机 + 重绘回原视图。
|
||||||
|
cam->SetPosition(pos);
|
||||||
|
cam->SetFocalPoint(fp);
|
||||||
|
cam->SetViewUp(up);
|
||||||
|
cam->SetViewAngle(va);
|
||||||
|
cam->SetParallelScale(ps);
|
||||||
|
cam->SetClippingRange(clip);
|
||||||
|
win->Render();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3],
|
||||||
|
const double p2[3],
|
||||||
|
const std::vector<std::array<double, 3>>& worldPts, int markType,
|
||||||
|
const std::string& outlineHex, const std::string& path, int& outW,
|
||||||
|
int& outH) {
|
||||||
|
outW = outH = 0;
|
||||||
|
if (colorImg == nullptr || worldPts.empty() || path.empty()) return false;
|
||||||
|
int dims[3];
|
||||||
|
colorImg->GetDimensions(dims);
|
||||||
|
const int nx = dims[0], ny = dims[1];
|
||||||
|
if (nx < 2 || ny < 2) return false;
|
||||||
|
|
||||||
|
// 平面两轴(image i↔e1=p1-o, j↔e2=p2-o);世界点 → 归一(u,v) → 像素(QImage 顶左原点,需翻 j)。
|
||||||
|
const double e1[3] = {p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]};
|
||||||
|
const double e2[3] = {p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]};
|
||||||
|
const double L1 = e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2];
|
||||||
|
const double L2 = e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2];
|
||||||
|
if (L1 < 1e-12 || L2 < 1e-12) return false;
|
||||||
|
QPolygonF poly;
|
||||||
|
poly.reserve(static_cast<int>(worldPts.size()));
|
||||||
|
for (const auto& P : worldPts) {
|
||||||
|
const double d[3] = {P[0] - o[0], P[1] - o[1], P[2] - o[2]};
|
||||||
|
const double u = (d[0] * e1[0] + d[1] * e1[1] + d[2] * e1[2]) / L1;
|
||||||
|
const double v = (d[0] * e2[0] + d[1] * e2[1] + d[2] * e2[2]) / L2;
|
||||||
|
poly << QPointF(u * (nx - 1), (ny - 1) - v * (ny - 1)); // 翻 j 到 QImage 坐标
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓冲半径:异常包围盒对角的 15%,最小取图长边 4%(点/小异常也有可见外扩)。
|
||||||
|
const QRectF pb = poly.boundingRect();
|
||||||
|
const double diag = std::hypot(pb.width(), pb.height());
|
||||||
|
const double buffer = std::max(0.04 * std::max(nx, ny), 0.15 * diag);
|
||||||
|
|
||||||
|
// 按形态构 buffer 后的裁剪形状:点→圆、线→胶囊带、面→外扩多边形(填充 ∪ 描边)。
|
||||||
|
QPainterPath shape;
|
||||||
|
if (markType == 1 || poly.size() == 1) {
|
||||||
|
shape.addEllipse(poly.first(), buffer, buffer);
|
||||||
|
} else if (markType == 2) {
|
||||||
|
QPainterPath line;
|
||||||
|
line.moveTo(poly.first());
|
||||||
|
for (int i = 1; i < poly.size(); ++i) line.lineTo(poly[i]);
|
||||||
|
QPainterPathStroker st;
|
||||||
|
st.setWidth(2.0 * buffer);
|
||||||
|
st.setCapStyle(Qt::RoundCap);
|
||||||
|
st.setJoinStyle(Qt::RoundJoin);
|
||||||
|
shape = st.createStroke(line);
|
||||||
|
} else {
|
||||||
|
QPainterPath fill;
|
||||||
|
fill.addPolygon(poly);
|
||||||
|
fill.closeSubpath();
|
||||||
|
QPainterPath outline = fill;
|
||||||
|
QPainterPathStroker st;
|
||||||
|
st.setWidth(2.0 * buffer);
|
||||||
|
st.setJoinStyle(Qt::RoundJoin);
|
||||||
|
shape = fill.united(st.createStroke(outline)); // 向外扩 buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// 裁剪区 = 形状包围盒(夹到图内)。
|
||||||
|
const QRect crop = shape.boundingRect().toAlignedRect().intersected(QRect(0, 0, nx, ny));
|
||||||
|
if (crop.width() < 1 || crop.height() < 1) return false;
|
||||||
|
|
||||||
|
// 切片着色图(vtk, j=0 在底) → QImage(顶左原点,翻行)。RGBA 保留外区透明(消除血缘外蓝边)。
|
||||||
|
const int comps = colorImg->GetNumberOfScalarComponents();
|
||||||
|
const bool rgba = comps >= 4;
|
||||||
|
QImage src(nx, ny, rgba ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
|
||||||
|
for (int j = 0; j < ny; ++j) {
|
||||||
|
uchar* row = src.scanLine(ny - 1 - j);
|
||||||
|
for (int i = 0; i < nx; ++i) {
|
||||||
|
const auto* px = static_cast<unsigned char*>(colorImg->GetScalarPointer(i, j, 0));
|
||||||
|
if (rgba) {
|
||||||
|
row[i * 4] = px[0];
|
||||||
|
row[i * 4 + 1] = px[1];
|
||||||
|
row[i * 4 + 2] = px[2];
|
||||||
|
row[i * 4 + 3] = px[3];
|
||||||
|
} else {
|
||||||
|
row[i * 3] = px[0];
|
||||||
|
row[i * 3 + 1] = px[1];
|
||||||
|
row[i * 3 + 2] = px[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出:buffer 形状内贴剖面像素(外透明),再描异常轮廓。
|
||||||
|
QImage out(crop.size(), QImage::Format_ARGB32);
|
||||||
|
out.fill(Qt::transparent);
|
||||||
|
QPainter pr(&out);
|
||||||
|
pr.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
pr.translate(-crop.topLeft());
|
||||||
|
pr.setClipPath(shape);
|
||||||
|
pr.drawImage(0, 0, src);
|
||||||
|
pr.setClipping(false);
|
||||||
|
QColor oc(QString::fromStdString(outlineHex));
|
||||||
|
if (!oc.isValid()) oc = QColor(255, 48, 48);
|
||||||
|
QPen pen(oc);
|
||||||
|
pen.setWidthF(2.0);
|
||||||
|
pr.setPen(pen);
|
||||||
|
pr.setBrush(Qt::NoBrush);
|
||||||
|
if (markType == 1 || poly.size() == 1)
|
||||||
|
pr.drawEllipse(poly.first(), 4.0, 4.0); // 点:小标记
|
||||||
|
else if (markType == 2)
|
||||||
|
pr.drawPolyline(poly);
|
||||||
|
else
|
||||||
|
pr.drawPolygon(poly);
|
||||||
|
pr.end();
|
||||||
|
|
||||||
|
if (!out.save(QString::fromStdString(path), "PNG")) return false;
|
||||||
|
outW = out.width();
|
||||||
|
outH = out.height();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
|
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
|
||||||
if (slice == nullptr || path.empty()) return false;
|
if (slice == nullptr || path.empty()) return false;
|
||||||
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class vtkImageData;
|
class vtkImageData;
|
||||||
class vtkRenderWindow;
|
class vtkRenderWindow;
|
||||||
|
|
@ -12,6 +14,26 @@ bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path);
|
||||||
// 截整个渲染窗口为 PNG(异常标识截图,需求 R88);成功返回 true,并填回截图像素宽高。
|
// 截整个渲染窗口为 PNG(异常标识截图,需求 R88);成功返回 true,并填回截图像素宽高。
|
||||||
bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& outW, int& outH);
|
bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& outW, int& outH);
|
||||||
|
|
||||||
|
// 「相机重构图」截图(方案A):把相机临时重新取景到 regionBounds(圈定范围)外扩后的区域,
|
||||||
|
// 使异常框在画面中央带周边语境,再截图、还原相机。业界 frame/zoom-to-fit selection 范式。
|
||||||
|
// regionBounds: {xmin,xmax,ymin,ymax,zmin,zmax} 世界系圈定包围盒;
|
||||||
|
// padFactor: 以盒中心外扩的倍数(1.4≈异常占画面~70%);
|
||||||
|
// minExtent: 退化兜底(点=零体积、线/面共面=某轴零厚度)时各轴的最小世界尺寸。
|
||||||
|
// 视角方向不变(仅推近/缩放);屏幕无闪(后台缓冲+关交换)。失败回退整窗截图。
|
||||||
|
bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor,
|
||||||
|
double minExtent, const std::string& path, int& outW, int& outH);
|
||||||
|
|
||||||
|
// 异常截图(正确做法):**只从切片那张 2D 剖面彩图**上,按异常几何**向外缓冲(buffer)一圈后裁剪**输出。
|
||||||
|
// 业界范式 = GIS「几何缓冲 + 按掩膜裁剪栅格」:点→圆、线→胶囊带、面→外扩多边形;缓冲外透明。
|
||||||
|
// colorImg:selectedSliceColorImage() 的剖面 RGB 图;o/p1/p2:该切片平面三点(image i↔p1-o, j↔p2-o);
|
||||||
|
// worldPts:异常顶点(世界系,落在该平面);markType:1点/2线/3面;outlineHex:在裁图上描异常轮廓的颜色。
|
||||||
|
// 成功返回 true,填回输出像素宽高。失败(无图/几何退化)返回 false,调用方可回退。
|
||||||
|
bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3],
|
||||||
|
const double p2[3],
|
||||||
|
const std::vector<std::array<double, 3>>& worldPts, int markType,
|
||||||
|
const std::string& outlineHex, const std::string& path, int& outW,
|
||||||
|
int& outH);
|
||||||
|
|
||||||
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。
|
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。
|
||||||
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,9 +232,11 @@ void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int&
|
||||||
const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米)
|
const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米)
|
||||||
const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米)
|
const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米)
|
||||||
|
|
||||||
// 距离上限(按剖面范围动态):数据中心在局部原点(0,0);瓦片离它太远则不加载——远裁剪面有界
|
// 距离上限(按剖面范围动态):以覆盖中心(相机焦点 cenX_,cenY_)为心,瓦片离它太远则不加载——
|
||||||
// (剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(其近端仍在范围内即保留)。
|
// 远裁剪面有界(剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(近端仍在范围内即保留)。
|
||||||
if (std::sqrt(cx * cx + cy * cy) - g * 0.5 > maxTileDist_) return;
|
// 心改用焦点而非原点(0,0):否则 frame 锚在别处数据(如深圳)时,看台湾数据全被剔除→底图空。
|
||||||
|
const double rx = cx - cenX_, ry = cy - cenY_;
|
||||||
|
if (std::sqrt(rx * rx + ry * ry) - g * 0.5 > maxTileDist_) return;
|
||||||
|
|
||||||
// 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)。
|
// 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)。
|
||||||
double screenPx;
|
double screenPx;
|
||||||
|
|
@ -286,6 +288,10 @@ void TileBasemap::refresh() {
|
||||||
if (pl[0] * fp[0] + pl[1] * fp[1] + pl[2] * fp[2] + pl[3] < 0.0)
|
if (pl[0] * fp[0] + pl[1] * fp[1] + pl[2] * fp[2] + pl[3] < 0.0)
|
||||||
for (int k = 0; k < 4; ++k) pl[k] = -pl[k];
|
for (int k = 0; k < 4; ++k) pl[k] = -pl[k];
|
||||||
}
|
}
|
||||||
|
// 底图覆盖中心 = 相机焦点(用户正看处)的局部 XY,而非世界原点:frame 锚在首个数据集,看远处别处
|
||||||
|
// 数据时原点离视野很远会把全部瓦片距离剔除→底图空。焦点为心则底图随视野走(同 frame 仍与数据对齐)。
|
||||||
|
cenX_ = fp[0];
|
||||||
|
cenY_ = fp[1];
|
||||||
|
|
||||||
// 底图最大距离按当前剖面合并范围动态定(随勾选增删自动伸缩);无数据用下限。
|
// 底图最大距离按当前剖面合并范围动态定(随勾选增删自动伸缩);无数据用下限。
|
||||||
maxTileDist_ = kRangeFloor;
|
maxTileDist_ = kRangeFloor;
|
||||||
|
|
@ -294,10 +300,10 @@ void TileBasemap::refresh() {
|
||||||
if (r > 0.0) maxTileDist_ = std::clamp(r * kRangeFactor, kRangeFloor, kRangeCeil);
|
if (r > 0.0) maxTileDist_ = std::clamp(r * kRangeFactor, kRangeFloor, kRangeCeil);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 四叉树:从数据中心一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无单层级盲区。
|
// 四叉树:从覆盖中心(相机焦点经纬)一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无盲区。
|
||||||
desired_.clear();
|
desired_.clear();
|
||||||
int count = 0;
|
int count = 0;
|
||||||
const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心
|
const auto c = frame_->toLatLon(cenX_, cenY_); // 覆盖中心 = 相机焦点(非世界原点)
|
||||||
const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom);
|
const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom);
|
||||||
for (int dy = -1; dy <= 1; ++dy)
|
for (int dy = -1; dy <= 1; ++dy)
|
||||||
for (int dx = -1; dx <= 1; ++dx)
|
for (int dx = -1; dx <= 1; ++dx)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,10 @@ private:
|
||||||
int satMaxZoom_ = 18;
|
int satMaxZoom_ = 18;
|
||||||
// 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。
|
// 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。
|
||||||
double camX_ = 0, camY_ = 0, camZ_ = 0;
|
double camX_ = 0, camY_ = 0, camZ_ = 0;
|
||||||
|
// 底图覆盖中心(相机焦点的局部 XY):四叉树根块取此处经纬、距离剔除以此为心。
|
||||||
|
// 关键——不能用世界原点(0,0):frame 锚在首个数据集(如深圳),看远处别处数据(如台湾,相距数百公里)时
|
||||||
|
// 原点离视野数百公里→全部瓦片被距离剔除→底图空。改用焦点→底图随视野走(瓦片与数据同 frame 仍对齐)。
|
||||||
|
double cenX_ = 0, cenY_ = 0;
|
||||||
double projK_ = 1.0;
|
double projK_ = 1.0;
|
||||||
bool projParallel_ = false;
|
bool projParallel_ = false;
|
||||||
double frustum_[24] = {0}; // 6 个视锥平面(内法向),AABB 全在某面外则剔除
|
double frustum_[24] = {0}; // 6 个视锥平面(内法向),AABB 全在某面外则剔除
|
||||||
|
|
|
||||||
|
|
@ -119,48 +119,6 @@ QPixmap renderAvatar(const QString& initials, int px, const QColor& bg, const QC
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
|
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
|
||||||
QMenu* buildViewMenu(QWidget* p)
|
|
||||||
{
|
|
||||||
auto* m = new QMenu(QStringLiteral("视图"), p);
|
|
||||||
m->addAction(QStringLiteral("分析视图"));
|
|
||||||
m->addAction(QStringLiteral("大屏视图"));
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMenu* buildProjectMenu(QWidget* p)
|
|
||||||
{
|
|
||||||
auto* m = new QMenu(QStringLiteral("项目管理"), p);
|
|
||||||
m->addAction(QStringLiteral("数据视图"));
|
|
||||||
auto* cfg = m->addMenu(QStringLiteral("项目配置"));
|
|
||||||
cfg->addAction(QStringLiteral("基本信息"));
|
|
||||||
cfg->addAction(QStringLiteral("项目结构"));
|
|
||||||
cfg->addAction(QStringLiteral("视图配置"));
|
|
||||||
m->addAction(QStringLiteral("数据管理"));
|
|
||||||
auto* biz = m->addMenu(QStringLiteral("业务管理"));
|
|
||||||
biz->addAction(QStringLiteral("异常管理"));
|
|
||||||
biz->addAction(QStringLiteral("异常体管理"));
|
|
||||||
auto* mon = m->addMenu(QStringLiteral("在线监测"));
|
|
||||||
mon->addAction(QStringLiteral("项目设备"));
|
|
||||||
mon->addAction(QStringLiteral("在线任务管理"));
|
|
||||||
auto* doc = m->addMenu(QStringLiteral("项目资料管理"));
|
|
||||||
doc->addAction(QStringLiteral("项目资料管理"));
|
|
||||||
doc->addAction(QStringLiteral("报告列表"));
|
|
||||||
auto* tools = m->addMenu(QStringLiteral("工具组件"));
|
|
||||||
tools->addAction(QStringLiteral("装置与脚本"));
|
|
||||||
tools->addAction(QStringLiteral("色阶配置"));
|
|
||||||
tools->addAction(QStringLiteral("异常类型管理"));
|
|
||||||
tools->addAction(QStringLiteral("模型管理"));
|
|
||||||
auto* exp = m->addMenu(QStringLiteral("批量导出"));
|
|
||||||
exp->addAction(QStringLiteral("文件导出"));
|
|
||||||
exp->addAction(QStringLiteral("报告导出"));
|
|
||||||
auto* alarm = m->addMenu(QStringLiteral("告警管理"));
|
|
||||||
alarm->addAction(QStringLiteral("设备告警"));
|
|
||||||
alarm->addAction(QStringLiteral("告警查询"));
|
|
||||||
m->addAction(QStringLiteral("自动任务"));
|
|
||||||
m->addAction(QStringLiteral("模板管理"));
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMenu* buildToolsMenu(QWidget* p)
|
QMenu* buildToolsMenu(QWidget* p)
|
||||||
{
|
{
|
||||||
auto* m = new QMenu(QStringLiteral("业务工具"), p);
|
auto* m = new QMenu(QStringLiteral("业务工具"), p);
|
||||||
|
|
@ -259,8 +217,8 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
|
|
||||||
// 一级菜单 → 工具条按钮(视图/项目管理/业务工具/设备),纯文字 + 下拉箭头。
|
// 一级菜单 → 工具条按钮(视图/项目管理/业务工具/设备),纯文字 + 下拉箭头。
|
||||||
// 复用原菜单构造器;菜单作为 popup 挂到按钮(按钮文字取菜单标题)。
|
// 复用原菜单构造器;菜单作为 popup 挂到按钮(按钮文字取菜单标题)。
|
||||||
lay->addWidget(makeMenuButton(this, buildViewMenu(this)));
|
lay->addWidget(makeMenuButton(this, buildViewMenu()));
|
||||||
lay->addWidget(makeMenuButton(this, buildProjectMenu(this)));
|
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(this)));
|
||||||
|
|
||||||
|
|
@ -332,6 +290,49 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
||||||
lay->addWidget(userRow_);
|
lay->addWidget(userRow_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视图菜单。「分析视图」=默认工作台(emit analysisViewRequested,中央区从 web 整窗切回工作台);
|
||||||
|
// 「大屏视图」当前为占位。
|
||||||
|
QMenu* TopBar::buildViewMenu() {
|
||||||
|
auto* m = new QMenu(QStringLiteral("视图"), this);
|
||||||
|
QObject::connect(m->addAction(QStringLiteral("分析视图")), &QAction::triggered, this,
|
||||||
|
[this] { emit analysisViewRequested(); });
|
||||||
|
m->addAction(QStringLiteral("大屏视图"));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 项目管理菜单。仅保留需「直接嵌入」的 web 页(Excel「单个项目」页签第 10~21 行带嵌入地址者):
|
||||||
|
// 在线监测 / 工具组件 / 批量导出 / 告警管理,点击 emit webPageRequested,由 main 独立整窗加载。
|
||||||
|
// 其余全部隐藏(数据视图、项目配置、数据管理、业务管理、项目资料管理、自动任务、模板管理 …)。
|
||||||
|
QMenu* TopBar::buildProjectMenu() {
|
||||||
|
auto* m = new QMenu(QStringLiteral("项目管理"), this);
|
||||||
|
// web 叶子项:携带 target 路径,点击发信号。
|
||||||
|
auto addWeb = [this](QMenu* parent, const QString& title, const QString& target) {
|
||||||
|
auto* a = parent->addAction(title);
|
||||||
|
QObject::connect(a, &QAction::triggered, this,
|
||||||
|
[this, title, target] { emit webPageRequested(title, target); });
|
||||||
|
};
|
||||||
|
|
||||||
|
auto* mon = m->addMenu(QStringLiteral("在线监测"));
|
||||||
|
addWeb(mon, QStringLiteral("项目设备"), QStringLiteral("/projectSpace/onlineMonitor/projectDevice"));
|
||||||
|
addWeb(mon, QStringLiteral("在线任务管理"), QStringLiteral("/projectSpace/onlineMonitor/onlineTask"));
|
||||||
|
|
||||||
|
auto* tools = m->addMenu(QStringLiteral("工具组件"));
|
||||||
|
addWeb(tools, QStringLiteral("装置与脚本"), QStringLiteral("/projectSpace/toolComponent/deviceScript"));
|
||||||
|
addWeb(tools, QStringLiteral("色阶配置"), QStringLiteral("/projectSpace/toolComponent/levelConfigure"));
|
||||||
|
addWeb(tools, QStringLiteral("异常类型管理"), QStringLiteral("/projectSpace/toolComponent/exceptionType"));
|
||||||
|
addWeb(tools, QStringLiteral("模型管理"), QStringLiteral("/projectSpace/toolComponent/modelManage"));
|
||||||
|
|
||||||
|
auto* exp = m->addMenu(QStringLiteral("批量导出"));
|
||||||
|
addWeb(exp, QStringLiteral("文件导出"), QStringLiteral("/projectSpace/bulkExport/fileExport"));
|
||||||
|
addWeb(exp, QStringLiteral("报告导出"), QStringLiteral("/projectSpace/bulkExport/templateExport"));
|
||||||
|
|
||||||
|
auto* alarm = m->addMenu(QStringLiteral("告警管理"));
|
||||||
|
addWeb(alarm, QStringLiteral("设备告警"), QStringLiteral("/projectSpace/alarmManage/deviceAlarm"));
|
||||||
|
addWeb(alarm, QStringLiteral("告警查询"), QStringLiteral("/projectSpace/alarmManage/alarmQuery"));
|
||||||
|
|
||||||
|
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_)
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,14 @@ signals:
|
||||||
void allProjectsRequested(); // 点击"全部项目…"
|
void allProjectsRequested(); // 点击"全部项目…"
|
||||||
void logoutRequested(); // 头像菜单「退出登录」
|
void logoutRequested(); // 头像菜单「退出登录」
|
||||||
void settingsRequested(); // 点击齿轮图标 → 打开设置
|
void settingsRequested(); // 点击齿轮图标 → 打开设置
|
||||||
|
// 项目管理菜单中「直接嵌入」的 web 页被点击:title=窗口标题,target=嵌入页 target 路径。
|
||||||
|
void webPageRequested(const QString& title, const QString& target);
|
||||||
|
void analysisViewRequested(); // 视图菜单「分析视图」→ 中央区切回默认工作台
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QMenu* buildViewMenu(); // 视图菜单(成员:「分析视图」需 emit 信号)
|
||||||
|
QMenu* buildProjectMenu(); // 项目管理菜单(成员:webview 叶子项需 emit 信号)
|
||||||
|
|
||||||
QToolButton* wsBtn_ = nullptr;
|
QToolButton* wsBtn_ = nullptr;
|
||||||
QToolButton* projBtn_ = nullptr;
|
QToolButton* projBtn_ = nullptr;
|
||||||
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)
|
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
#include <vtkActor.h>
|
#include <vtkActor.h>
|
||||||
#include <vtkProperty.h>
|
#include <vtkProperty.h>
|
||||||
#include <vtkBoundingBox.h>
|
#include <vtkBoundingBox.h>
|
||||||
|
#include <vtkCellPicker.h>
|
||||||
#include <vtkCubeAxesActor.h>
|
#include <vtkCubeAxesActor.h>
|
||||||
|
#include <vtkNew.h>
|
||||||
#include <vtkProp.h>
|
#include <vtkProp.h>
|
||||||
#include <vtkRenderWindow.h>
|
#include <vtkRenderWindow.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
|
|
@ -80,12 +82,14 @@ void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VtkSceneView::computeDataBounds(double out[6]) const {
|
bool VtkSceneView::computeDataBounds(double out[6]) const {
|
||||||
|
// 仅计「可见」prop:二维分析下 3D 体/帘面已隐藏,取景/坐标轴/底图范围都应只围当前可见维度,
|
||||||
|
// 否则二维取景被隐藏的远处 3D 体撑歪、坐标轴框错维度。
|
||||||
vtkBoundingBox bb;
|
vtkBoundingBox bb;
|
||||||
for (const auto& kv : dsProps_)
|
for (const auto& kv : dsProps_)
|
||||||
for (const auto& p : kv.second)
|
for (const auto& p : kv.second)
|
||||||
if (p) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||||
for (const auto& p : miscProps_)
|
for (const auto& p : miscProps_)
|
||||||
if (p) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||||
if (!bb.IsValid()) return false;
|
if (!bb.IsValid()) return false;
|
||||||
bb.GetBounds(out);
|
bb.GetBounds(out);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -102,6 +106,8 @@ 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 足迹维度记录随数据图元一并清(模式标志保留)
|
||||||
|
selectedMapLines_.clear(); // 选中态随图元清(actor 已销毁);Z 偏移 mapLineZOffset_ 保留→重建后复位高度
|
||||||
removeProps(miscProps_);
|
removeProps(miscProps_);
|
||||||
clearAnomalies(); // 异常 actor 随清场一并移除
|
clearAnomalies(); // 异常 actor 随清场一并移除
|
||||||
if (currentAxes_) {
|
if (currentAxes_) {
|
||||||
|
|
@ -149,6 +155,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -167,6 +174,7 @@ 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;
|
||||||
|
|
@ -187,8 +195,12 @@ void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLi
|
||||||
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_);
|
auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_);
|
||||||
if (actor) {
|
if (actor) {
|
||||||
|
actor->SetVisibility(analysisMode2D_ ? 1 : 0); // 足迹=2D内容:仅二维分析下显示
|
||||||
|
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 按维度翻可见)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,6 +218,10 @@ void VtkSceneView::removeDataset(const std::string& dsId) {
|
||||||
if (it == dsProps_.end()) return;
|
if (it == dsProps_.end()) return;
|
||||||
removeProps(it->second);
|
removeProps(it->second);
|
||||||
dsProps_.erase(it);
|
dsProps_.erase(it);
|
||||||
|
mapLineDs_.erase(dsId); // 若是 2D 足迹则同步去除维度记录
|
||||||
|
// 场景已无任何数据图元 → 复位重锚标志:下个数据(可能在别处)重新把 frame 锚到它,底图随之归位。
|
||||||
|
// 否则删到空再加远处新数据时,新数据按旧锚点投到偏远世界坐标、底图仍贴在旧位置 → 底图"消失"。
|
||||||
|
if (dsProps_.empty()) frameAnchoredToData_ = false;
|
||||||
const bool wasVolume = volumes_.erase(dsId) > 0;
|
const bool wasVolume = volumes_.erase(dsId) > 0;
|
||||||
if (volumeOwnerDs_ == dsId) { // 移除的是"当前体" → currentImage 回退到剩余某体,无则置空
|
if (volumeOwnerDs_ == dsId) { // 移除的是"当前体" → currentImage 回退到剩余某体,无则置空
|
||||||
if (!volumes_.empty()) {
|
if (!volumes_.empty()) {
|
||||||
|
|
@ -303,6 +319,137 @@ void VtkSceneView::fitView() {
|
||||||
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
|
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VtkSceneView::setAnalysisMode2D(bool is2D) {
|
||||||
|
if (is2D == analysisMode2D_) return; // 幂等:同模式重复切不做事
|
||||||
|
analysisMode2D_ = is2D;
|
||||||
|
if (!is2D) clearMapLineSelection(); // 离开二维分析:清足迹选中(三维下不可拖 Z);Z 偏移仍持久
|
||||||
|
|
||||||
|
// ① 按维度翻可见标志(不清空、不重建→切换瞬时):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;
|
||||||
|
for (auto& p : it->second) {
|
||||||
|
auto* a = vtkActor::SafeDownCast(p);
|
||||||
|
if (a) a->AddPosition(0.0, 0.0, worldDz); // 仅改 Z,锁 XY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scene_.renderer()) scene_.renderer()->ResetCameraClippingRange(); // Z 抬升后防被裁剪面切
|
||||||
|
if (renderWindow_) renderWindow_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
double VtkSceneView::selectedMapLineZ() const {
|
||||||
|
if (selectedMapLines_.empty()) return 0.0;
|
||||||
|
// 代表性 Z = 任一选中足迹 actor 的包围盒中心 Z(含 placement worldZ + 已累计偏移)。
|
||||||
|
auto it = dsProps_.find(*selectedMapLines_.begin());
|
||||||
|
if (it == dsProps_.end()) return 0.0;
|
||||||
|
for (const auto& p : it->second)
|
||||||
|
if (p) { if (double* b = p->GetBounds()) return 0.5 * (b[4] + b[5]); }
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
void VtkSceneView::rebuildAxes() {
|
void VtkSceneView::rebuildAxes() {
|
||||||
// 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render +
|
// 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render +
|
||||||
// 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。
|
// 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。
|
||||||
|
|
@ -310,6 +457,9 @@ 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,场景无坐标轴。
|
||||||
double bounds[6];
|
double bounds[6];
|
||||||
|
|
@ -340,14 +490,18 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
|
||||||
// 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。
|
// 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。
|
||||||
if (!is2D) rebuildAxes();
|
if (!is2D) rebuildAxes();
|
||||||
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
|
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
|
||||||
|
// 朝向优先看二维分析模式(本期 A):处二维分析→近俯视;否则按 ViewMode(旧 Map2D 正俯视/三维自由)。
|
||||||
|
// 这样 VE 改/项目切等全量重建在二维分析下仍保持近俯视,不跳回三维自由视角。
|
||||||
if (resetCamera) {
|
if (resetCamera) {
|
||||||
if (is2D)
|
if (analysisMode2D_)
|
||||||
|
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());
|
||||||
double bounds[6];
|
double bounds[6];
|
||||||
if (computeDataBounds(bounds))
|
if (computeDataBounds(bounds))
|
||||||
scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图,否则数据缩成小点)
|
scene_.renderer()->ResetCamera(bounds); // 取景到"可见"数据(不含底图,否则数据缩成小点)
|
||||||
else
|
else
|
||||||
scene_.renderer()->ResetCamera();
|
scene_.renderer()->ResetCamera();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
@ -82,6 +83,29 @@ 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 期:选中 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 包围盒中心:使数据落在世界原点近旁
|
||||||
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
|
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
|
||||||
|
|
@ -146,6 +170,16 @@ private:
|
||||||
std::vector<vtkSmartPointer<vtkProp>> miscProps_;
|
std::vector<vtkSmartPointer<vtkProp>> miscProps_;
|
||||||
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):切 tab 按此区分维度翻可见(其余 dsProps_=帘面/体=3D)。
|
||||||
|
std::set<std::string> mapLineDs_;
|
||||||
|
bool analysisMode2D_ = false; // 当前是否处二维分析(默认三维:启动在「三维分析」tab)
|
||||||
|
|
||||||
|
// B 期:选中的足迹 dsId(Z 拖动目标) + 各足迹累计 Z 偏移(持久,全量重建后 addMapLine 复用)。
|
||||||
|
std::set<std::string> selectedMapLines_;
|
||||||
|
std::map<std::string, double> mapLineZOffset_;
|
||||||
|
void applyMapLineSelectionVisual(); // 选中足迹加粗变亮、其余复原(橙 3.0)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,9 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
|
||||||
{"下", ViewDir::Bottom}, {"左", ViewDir::Left}, {"右", ViewDir::Right}};
|
{"下", ViewDir::Bottom}, {"左", ViewDir::Left}, {"右", ViewDir::Right}};
|
||||||
for (const V& v : views) {
|
for (const V& v : views) {
|
||||||
const ViewDir d = v.d;
|
const ViewDir d = v.d;
|
||||||
connect(textBtn(QString::fromUtf8(v.t)), &QToolButton::clicked, this,
|
auto* b = textBtn(QString::fromUtf8(v.t));
|
||||||
[this, d] { emit viewRequested(d); });
|
connect(b, &QToolButton::clicked, this, [this, d] { emit viewRequested(d); });
|
||||||
|
viewDirButtons_.push_back(b); // 二维分析下禁用(会改朝向、破坏近俯视锁定)
|
||||||
}
|
}
|
||||||
sep();
|
sep();
|
||||||
// ── 段3:缩放 / 复位 ──
|
// ── 段3:缩放 / 复位 ──
|
||||||
|
|
@ -84,4 +85,12 @@ 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,7 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <vector>
|
||||||
#include "I3dSceneView.hpp" // geopro::controller::ViewDir
|
#include "I3dSceneView.hpp" // geopro::controller::ViewDir
|
||||||
|
|
||||||
|
class QToolButton;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// VTK 画布竖排工具条(spec §9):全局视图控制——设置(坐标轴)/前后上下左右/放大缩小复位。
|
// VTK 画布竖排工具条(spec §9):全局视图控制——设置(坐标轴)/前后上下左右/放大缩小复位。
|
||||||
|
|
@ -11,12 +14,20 @@ class VtkViewToolbar : public QWidget {
|
||||||
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 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
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
||||||
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
|
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
|
||||||
"#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
|
"#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
|
||||||
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
|
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
|
||||||
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }")
|
// 验证码容器固定白底:后端验证码图是浅底,白底贴合图边(两种主题皆然,故用白字面值)。
|
||||||
|
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: #FFFFFF; }")
|
||||||
.arg(scaledPx(type::kDisplay))
|
.arg(scaledPx(type::kDisplay))
|
||||||
.arg(type::kWeightBold)
|
.arg(type::kWeightBold)
|
||||||
.arg(scaledPx(type::kCaption))
|
.arg(scaledPx(type::kCaption))
|
||||||
|
|
|
||||||
255
src/app/main.cpp
255
src/app/main.cpp
|
|
@ -50,10 +50,12 @@
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QLibraryInfo>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QTranslator>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
|
@ -64,6 +66,7 @@
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
#include <QStackedWidget>
|
||||||
#include <QStatusBar>
|
#include <QStatusBar>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
|
|
@ -111,6 +114,7 @@
|
||||||
#include "ProjectListDialog.hpp"
|
#include "ProjectListDialog.hpp"
|
||||||
#include "ObjectFormDialog.hpp"
|
#include "ObjectFormDialog.hpp"
|
||||||
#include "ImportDatasetDialog.hpp"
|
#include "ImportDatasetDialog.hpp"
|
||||||
|
#include "panels/web/ProjectWebView.hpp"
|
||||||
#include "WorkbenchNavController.hpp"
|
#include "WorkbenchNavController.hpp"
|
||||||
#include "VtkSceneController.hpp"
|
#include "VtkSceneController.hpp"
|
||||||
#include "VtkSceneView.hpp"
|
#include "VtkSceneView.hpp"
|
||||||
|
|
@ -150,6 +154,7 @@
|
||||||
#include "Scene.hpp"
|
#include "Scene.hpp"
|
||||||
#include "VoxelFromScatters.hpp"
|
#include "VoxelFromScatters.hpp"
|
||||||
#include "interact/InteractionManager.hpp"
|
#include "interact/InteractionManager.hpp"
|
||||||
|
#include "interact/PickInteractorStyle.hpp"
|
||||||
#include "interact/SlicePlaneMath.hpp"
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
#include "actors/AnomalyActor.hpp"
|
#include "actors/AnomalyActor.hpp"
|
||||||
#include "actors/CurtainActor.hpp"
|
#include "actors/CurtainActor.hpp"
|
||||||
|
|
@ -249,7 +254,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
geopro::data::IColorTemplateRepository& colorTplRepo,
|
geopro::data::IColorTemplateRepository& colorTplRepo,
|
||||||
geopro::data::IDatasetCommandRepository& cmdRepo,
|
geopro::data::IDatasetCommandRepository& cmdRepo,
|
||||||
geopro::controller::WorkbenchNavController& nav,
|
geopro::controller::WorkbenchNavController& nav,
|
||||||
geopro::controller::DatasetDetailController& detailCtrl)
|
geopro::controller::DatasetDetailController& detailCtrl,
|
||||||
|
const QString& sessionToken)
|
||||||
{
|
{
|
||||||
// ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ──
|
// ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ──
|
||||||
// 全项目共享(shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
|
// 全项目共享(shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
|
||||||
|
|
@ -322,7 +328,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
ads::CDockManager::setAutoHideConfigFlags(ads::CDockManager::AutoHideFlags()); // 禁用自动隐藏
|
ads::CDockManager::setAutoHideConfigFlags(ads::CDockManager::AutoHideFlags()); // 禁用自动隐藏
|
||||||
|
|
||||||
auto* dockManager = new ads::CDockManager(&window);
|
auto* dockManager = new ads::CDockManager(&window);
|
||||||
window.setCentralWidget(dockManager);
|
// 中央区用 QStackedWidget 承载:page0=工作台(dockManager,默认「分析视图」),
|
||||||
|
// page1=项目管理 web 页(点项目管理菜单整窗加载,视图菜单「分析视图」切回 page0)。
|
||||||
|
auto* centralStack = new QStackedWidget(&window);
|
||||||
|
centralStack->addWidget(dockManager); // index 0:工作台
|
||||||
|
window.setCentralWidget(centralStack);
|
||||||
|
|
||||||
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
|
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
|
||||||
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
|
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
|
||||||
|
|
@ -396,6 +406,74 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
viewToolbar->move(12, 12);
|
viewToolbar->move(12, 12);
|
||||||
viewToolbar->raise();
|
viewToolbar->raise();
|
||||||
viewToolbar->show();
|
viewToolbar->show();
|
||||||
|
// 异常绘制操作提示:右上角 QLabel 浮层(VTK 内置字体不含中文字形,故用 Qt 渲染中文 + QSS 美化)。
|
||||||
|
// 深底 + accent 描边;不挡画布鼠标事件;绘制开始显示、结束/取消隐藏(见 onSliceContextMenuRequested)。
|
||||||
|
auto* anomalyHint = new QLabel(vtkWidget);
|
||||||
|
anomalyHint->setObjectName(QStringLiteral("anomalyHint"));
|
||||||
|
anomalyHint->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
// 方角 + 不透明深底:避免「圆角外三角区露白底」与「半透明在 GL 子控件上渲染成灰」两个坑。
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
anomalyHint,
|
||||||
|
QStringLiteral("QLabel#anomalyHint{background:#0E1A2D;color:#E6ECF5;"
|
||||||
|
"border:1px solid {{accent/primary}};padding:8px 12px;}"));
|
||||||
|
anomalyHint->hide();
|
||||||
|
|
||||||
|
// ── 二维分析 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();
|
||||||
|
});
|
||||||
|
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);
|
||||||
axesPanel->hide();
|
axesPanel->hide();
|
||||||
|
|
@ -503,11 +581,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿);导出统一为「导出▸图片·dat」;
|
// 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿);导出统一为「导出▸图片·dat」;
|
||||||
// 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。
|
// 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。
|
||||||
interactionMgr->onSliceContextMenuRequested =
|
interactionMgr->onSliceContextMenuRequested =
|
||||||
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, refreshAnomalies, drawer,
|
[&window, &cmdRepo, &nav, interactionMgr, sceneView, scene3dRepo, refreshAnalysis,
|
||||||
anomalyDrawTool, renderWindowPtr]() {
|
refreshAnomalies, drawer, anomalyDrawTool, renderWindowPtr, anomalyHint, vtkWidget]() {
|
||||||
QMenu menu(&window);
|
QMenu menu(&window);
|
||||||
QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常"));
|
QMenu* anomMenu = menu.addMenu(QStringLiteral("创建异常")); // → 点/线/面 子菜单
|
||||||
QAction* aSave = menu.addAction(QStringLiteral("保存"));
|
QAction* aAnoPoint = anomMenu->addAction(QStringLiteral("点"));
|
||||||
|
QAction* aAnoLine = anomMenu->addAction(QStringLiteral("线"));
|
||||||
|
QAction* aAnoFace = anomMenu->addAction(QStringLiteral("面"));
|
||||||
|
// 「保存」仅对未保存(临时)切片显示——已保存切片定稿锁定、不可再改/再存(用户要求)。
|
||||||
|
QAction* aSave = interactionMgr->selectedSliceDsId().empty()
|
||||||
|
? menu.addAction(QStringLiteral("保存"))
|
||||||
|
: nullptr;
|
||||||
QMenu* expMenu = menu.addMenu(QStringLiteral("导出"));
|
QMenu* expMenu = menu.addMenu(QStringLiteral("导出"));
|
||||||
QAction* aImg = expMenu->addAction(QStringLiteral("图片"));
|
QAction* aImg = expMenu->addAction(QStringLiteral("图片"));
|
||||||
QAction* aDat = expMenu->addAction(QStringLiteral("dat"));
|
QAction* aDat = expMenu->addAction(QStringLiteral("dat"));
|
||||||
|
|
@ -521,27 +605,44 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (chosen == aFace) { interactionMgr->faceSelected(); return; }
|
if (chosen == aFace) { interactionMgr->faceSelected(); return; }
|
||||||
if (chosen == aFlip) { interactionMgr->flipView(); return; }
|
if (chosen == aFlip) { interactionMgr->flipView(); return; }
|
||||||
if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选
|
if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选
|
||||||
if (chosen == aAnomaly) {
|
if (chosen == aAnoPoint || chosen == aAnoLine || chosen == aAnoFace) {
|
||||||
// 在选中切片平面上启动圈定(左键逐点、右键/回车闭合、Esc 取消)。
|
// 形态(1点/2线/3面):同时决定绘制工具 mode、a.markType、对话框查平台类型的 remarkSourceType。
|
||||||
|
// core::AnomalyMarkType 与 remarkSourceType 同值(Point=1/Polyline=2/Polygon=3),用一个 shape 贯通。
|
||||||
namespace ri = geopro::render::interact;
|
namespace ri = geopro::render::interact;
|
||||||
|
using DM = ri::AnomalyDrawTool::DrawMode;
|
||||||
|
const int shape = (chosen == aAnoPoint) ? 1 : (chosen == aAnoLine) ? 2 : 3;
|
||||||
|
const DM mode =
|
||||||
|
(chosen == aAnoPoint) ? DM::Point : (chosen == aAnoLine) ? DM::Line : DM::Face;
|
||||||
int axis = 3;
|
int axis = 3;
|
||||||
ri::Vec3 o{}, p1{}, p2{};
|
ri::Vec3 o{}, p1{}, p2{};
|
||||||
if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return;
|
if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return;
|
||||||
const ri::Vec3 e1{{p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]}};
|
const ri::Vec3 e1{{p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]}};
|
||||||
const ri::Vec3 e2{{p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]}};
|
const ri::Vec3 e2{{p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]}};
|
||||||
const ri::Vec3 normal = ri::normalize(ri::cross(e1, e2));
|
const ri::Vec3 normal = ri::normalize(ri::cross(e1, e2));
|
||||||
|
// 操作提示浮层(右上角):按形态显示结束方式;绘制结束/取消隐藏。
|
||||||
|
anomalyHint->setText(
|
||||||
|
shape == 1 ? QStringLiteral("标注点\n左键单击落点即完成\nEsc 取消")
|
||||||
|
: shape == 2
|
||||||
|
? QStringLiteral("标注线\n左键逐点 · 双击结束\nBackspace 撤点 · Esc 取消")
|
||||||
|
: QStringLiteral("标注面\n左键逐点 · 点回起点闭合\nBackspace 撤点 · Esc 取消"));
|
||||||
|
anomalyHint->adjustSize();
|
||||||
|
anomalyHint->move(vtkWidget->width() - anomalyHint->width() - 12, 12); // 右上角
|
||||||
|
anomalyHint->show();
|
||||||
|
anomalyHint->raise();
|
||||||
// 多体并发:异常挂到"选中切片所属体"(非 currentVolume),无选中切片回退当前体。
|
// 多体并发:异常挂到"选中切片所属体"(非 currentVolume),无选中切片回退当前体。
|
||||||
std::string volId = interactionMgr->selectedSliceVolumeDsId();
|
std::string volId = interactionMgr->selectedSliceVolumeDsId();
|
||||||
if (volId.empty()) volId = sceneView->currentVolumeDsId();
|
if (volId.empty()) volId = sceneView->currentVolumeDsId();
|
||||||
// 异常归属(spec §8):当前选中切片已保存(selectedSliceDsId 非空)→挂该切片;临时切片→挂体。
|
// 异常归属(spec §8):当前选中切片已保存(selectedSliceDsId 非空)→挂该切片;临时切片→挂体。
|
||||||
const std::string savedSliceId = interactionMgr->selectedSliceDsId();
|
const std::string savedSliceId = interactionMgr->selectedSliceDsId();
|
||||||
anomalyDrawTool->start(
|
anomalyDrawTool->start(
|
||||||
o, normal,
|
mode, o, normal,
|
||||||
[&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies, refreshAnalysis,
|
[&window, &cmdRepo, &nav, interactionMgr, sceneView, scene3dRepo, renderWindowPtr,
|
||||||
volId, savedSliceId, normal, o](const std::vector<ri::Vec3>& worldPts) {
|
refreshAnomalies, refreshAnalysis, volId, savedSliceId, normal, o, p1, p2, shape,
|
||||||
|
anomalyHint](const std::vector<ri::Vec3>& worldPts) {
|
||||||
|
anomalyHint->hide(); // 绘制结束 → 隐藏操作提示
|
||||||
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
|
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
|
||||||
geopro::core::Anomaly a;
|
geopro::core::Anomaly a;
|
||||||
a.markType = geopro::core::AnomalyMarkType::Polygon;
|
a.markType = static_cast<geopro::core::AnomalyMarkType>(shape);
|
||||||
a.remarkSourceId =
|
a.remarkSourceId =
|
||||||
geopro::core::resolveAnomalyMount(!savedSliceId.empty(), savedSliceId, volId);
|
geopro::core::resolveAnomalyMount(!savedSliceId.empty(), savedSliceId, volId);
|
||||||
a.lineColor = "#ff3030";
|
a.lineColor = "#ff3030";
|
||||||
|
|
@ -554,12 +655,40 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
a.id = draftId;
|
a.id = draftId;
|
||||||
sceneView->addAnomaly(a);
|
sceneView->addAnomaly(a);
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
// 截图(含异常)→ 临时文件。
|
|
||||||
const QString shot =
|
const QString shot =
|
||||||
QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png"));
|
QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png"));
|
||||||
int sw = 0, sh = 0;
|
int sw = 0, sh = 0;
|
||||||
geopro::app::captureRenderWindowPng(renderWindowPtr, shot.toStdString(), sw, sh);
|
// 截图(正确做法):只从切片那张 2D 剖面彩图、按异常几何向外缓冲一圈裁剪(GIS buffer+掩膜)。
|
||||||
geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &window);
|
std::vector<std::array<double, 3>> wpts;
|
||||||
|
wpts.reserve(worldPts.size());
|
||||||
|
for (const auto& p : worldPts) wpts.push_back({p[0], p[1], p[2]});
|
||||||
|
vtkSmartPointer<vtkImageData> sliceColor = interactionMgr->selectedSliceColorImage();
|
||||||
|
const double oo[3] = {o[0], o[1], o[2]};
|
||||||
|
const double pp1[3] = {p1[0], p1[1], p1[2]};
|
||||||
|
const double pp2[3] = {p2[0], p2[1], p2[2]};
|
||||||
|
bool shotOk = sliceColor && geopro::app::captureAnomalyShotFromSlice(
|
||||||
|
sliceColor, oo, pp1, pp2, wpts, shape,
|
||||||
|
a.lineColor, shot.toStdString(), sw, sh);
|
||||||
|
if (!shotOk) { // 回退:无切片图时退回相机框景(整窗外扩),至少有图。
|
||||||
|
double rb[6] = {worldPts[0][0], worldPts[0][0], worldPts[0][1],
|
||||||
|
worldPts[0][1], worldPts[0][2], worldPts[0][2]};
|
||||||
|
for (const auto& p : worldPts) {
|
||||||
|
rb[0] = std::min(rb[0], p[0]); rb[1] = std::max(rb[1], p[0]);
|
||||||
|
rb[2] = std::min(rb[2], p[1]); rb[3] = std::max(rb[3], p[1]);
|
||||||
|
rb[4] = std::min(rb[4], p[2]); rb[5] = std::max(rb[5], p[2]);
|
||||||
|
}
|
||||||
|
auto vlen = [](double x, double y, double z) {
|
||||||
|
return std::sqrt(x * x + y * y + z * z);
|
||||||
|
};
|
||||||
|
const double e1 = vlen(p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]);
|
||||||
|
const double e2 = vlen(p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]);
|
||||||
|
const double minExt = 0.25 * std::min(e1, e2);
|
||||||
|
geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt,
|
||||||
|
shot.toStdString(), sw, sh);
|
||||||
|
}
|
||||||
|
// 异常类型按标注形态(shape=1点/2线/3面)拉对应平台类型,与平台一致。
|
||||||
|
geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &cmdRepo,
|
||||||
|
nav.currentProjectId(), shape, &window);
|
||||||
if (dlg.exec() != QDialog::Accepted) {
|
if (dlg.exec() != QDialog::Accepted) {
|
||||||
sceneView->removeAnomaly(draftId);
|
sceneView->removeAnomaly(draftId);
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
|
|
@ -570,6 +699,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
a.typeName = dlg.typeName().toStdString();
|
a.typeName = dlg.typeName().toStdString();
|
||||||
a.exceptionTypeId = dlg.typeId().toStdString();
|
a.exceptionTypeId = dlg.typeId().toStdString();
|
||||||
a.remark = dlg.remark().toStdString();
|
a.remark = dlg.remark().toStdString();
|
||||||
|
// 平台样式:选中异常类型的 legend 派生(与平台一致);未取到则保留上面的默认样式。
|
||||||
|
if (!dlg.styleColor().isEmpty()) {
|
||||||
|
a.lineColor = dlg.styleColor().toStdString();
|
||||||
|
if (dlg.styleWidth() > 0.0) a.lineWidth = dlg.styleWidth();
|
||||||
|
a.dashed = dlg.styleDashed();
|
||||||
|
}
|
||||||
scene3dRepo->saveAnomaly(
|
scene3dRepo->saveAnomaly(
|
||||||
a, shot.toStdString(),
|
a, shot.toStdString(),
|
||||||
[sceneView, renderWindowPtr, refreshAnomalies, refreshAnalysis,
|
[sceneView, renderWindowPtr, refreshAnomalies, refreshAnalysis,
|
||||||
|
|
@ -584,10 +719,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
QString::fromStdString(m));
|
QString::fromStdString(m));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[]() { /* onCancel:放弃,无需处理 */ });
|
[anomalyHint]() { anomalyHint->hide(); }); // 取消(Esc)→隐藏提示
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chosen == aSave) {
|
if (aSave != nullptr && chosen == aSave) {
|
||||||
int axis = 3;
|
int axis = 3;
|
||||||
geopro::render::interact::Vec3 o{}, p1{}, p2{};
|
geopro::render::interact::Vec3 o{}, p1{}, p2{};
|
||||||
if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return;
|
if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return;
|
||||||
|
|
@ -783,9 +918,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 三维体段右键删除:切片→deleteSlice / 异常→deleteAnomaly,删后刷新树。
|
// 三维体段右键删除:切片→deleteSlice / 异常→deleteAnomaly,删后刷新树。
|
||||||
// 异常删除须同时 refreshAnomalies(重载异常 actor)——否则列表行没了但场景里异常仍渲染(技术债,已修)。
|
// 异常删除须同时 refreshAnomalies(重载异常 actor)——否则列表行没了但场景里异常仍渲染(技术债,已修)。
|
||||||
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::deleteDatasetRequested, &window,
|
QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::deleteDatasetRequested, &window,
|
||||||
[scene3dRepo, refreshAnalysis, refreshAnomalies](const QString& dsId,
|
[scene3dRepo, refreshAnalysis, refreshAnomalies, &window](const QString& dsId,
|
||||||
const QString& ddCode) {
|
const QString& ddCode) {
|
||||||
const std::string id = dsId.toStdString();
|
const std::string id = dsId.toStdString();
|
||||||
|
// 删除前确认(不可撤销):明确中文「删除/取消」按钮。
|
||||||
|
const QString what =
|
||||||
|
ddCode == QStringLiteral("dd_slice") ? QStringLiteral("切片")
|
||||||
|
: QStringLiteral("异常");
|
||||||
|
QMessageBox box(QMessageBox::Warning, QStringLiteral("删除%1").arg(what),
|
||||||
|
QStringLiteral("确定删除该%1吗?此操作不可撤销。").arg(what),
|
||||||
|
QMessageBox::NoButton, &window);
|
||||||
|
QPushButton* del = box.addButton(QStringLiteral("删除"), QMessageBox::AcceptRole);
|
||||||
|
box.addButton(QStringLiteral("取消"), QMessageBox::RejectRole);
|
||||||
|
box.exec();
|
||||||
|
if (box.clickedButton() != del) return; // 取消 → 不删
|
||||||
if (ddCode == QStringLiteral("dd_slice")) {
|
if (ddCode == QStringLiteral("dd_slice")) {
|
||||||
scene3dRepo->deleteSlice(
|
scene3dRepo->deleteSlice(
|
||||||
id, [refreshAnalysis]() { refreshAnalysis(); },
|
id, [refreshAnalysis]() { refreshAnalysis(); },
|
||||||
|
|
@ -932,19 +1078,35 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
[sceneView, interactionMgr, renderWindowPtr](const QString& dsId,
|
[sceneView, interactionMgr, renderWindowPtr](const QString& dsId,
|
||||||
const QString& ddCode) {
|
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")) {
|
||||||
sceneView->setSelectedAnomaly(id);
|
sceneView->setSelectedAnomaly(id);
|
||||||
else if (ddCode == QStringLiteral("dd_slice"))
|
interactionMgr->deselectSlice();
|
||||||
|
} else if (ddCode == QStringLiteral("dd_slice")) {
|
||||||
|
sceneView->setSelectedAnomaly(std::string{});
|
||||||
interactionMgr->selectSavedSlice(id); // 选中已渲染的该切片(高亮)
|
interactionMgr->selectSavedSlice(id); // 选中已渲染的该切片(高亮)
|
||||||
|
} else {
|
||||||
|
sceneView->setSelectedAnomaly(std::string{});
|
||||||
|
interactionMgr->deselectSlice();
|
||||||
|
}
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
});
|
});
|
||||||
// 反向 VTK→list:在 VTK 里点中/选中一张切片 → 在三维体段树里同步选中该切片行(②反向)。
|
// 反向 VTK→list:在 VTK 里点中/选中一张切片 → 在三维体段树里同步选中该切片行(②反向)。
|
||||||
interactionMgr->onSliceSelectionChanged = [drawer](const std::string& dsId) {
|
// 点空白/清选(dsId 空) → 一并清 VTK 异常高亮(否则取消选中后异常图形仍高亮,用户反馈)。
|
||||||
|
interactionMgr->onSliceSelectionChanged = [drawer, sceneView, renderWindowPtr](
|
||||||
|
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()) {
|
||||||
|
sceneView->setSelectedAnomaly(std::string{});
|
||||||
|
renderWindowPtr->Render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 已保存切片经 VTK 右键「关闭」→ 取消数据列表对应切片项的勾选(否则列表仍勾选,用户反馈)。
|
||||||
|
interactionMgr->onSliceClosed = [drawer](const std::string& dsId) {
|
||||||
|
if (auto* sec = drawer->analysisTab()->section("voxel"))
|
||||||
|
sec->setChecked(QString::fromStdString(dsId), false);
|
||||||
};
|
};
|
||||||
// 异常双击属性(R83)/右键删除已并入 analysisTab 的 detailRequested(dd_anomaly) /
|
|
||||||
// deleteDatasetRequested(dd_anomaly);列表选中→VTK高亮(R84)随旧栏退役暂缺,待新段补 anomalySelected。
|
|
||||||
|
|
||||||
// ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token,经同一共享 GeoLocalFrame 配准)──
|
// ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token,经同一共享 GeoLocalFrame 配准)──
|
||||||
auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window);
|
auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window);
|
||||||
|
|
@ -980,6 +1142,19 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (*view2dMode == 4) sceneCtrl->set2DPlacement(4, 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](bool is2D) {
|
||||||
|
interactionMgr->setMode2D(is2D);
|
||||||
|
sceneCtrl->onAnalysisModeChanged(is2D);
|
||||||
|
sceneView->setAnalysisMode2D(is2D);
|
||||||
|
viewToolbar->setAnalysisMode2D(is2D); // 二维下禁用 6 向快捷视图
|
||||||
|
});
|
||||||
|
|
||||||
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
|
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
|
||||||
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
|
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
|
||||||
sceneView->onFrameReanchored = [basemap, basemapKind]() {
|
sceneView->onFrameReanchored = [basemap, basemapKind]() {
|
||||||
|
|
@ -1071,6 +1246,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
vtkDock->setWidget(centerWidget);
|
vtkDock->setWidget(centerWidget);
|
||||||
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
||||||
|
|
||||||
|
// 项目管理「直接嵌入」web 页:作为中央 QStackedWidget 的第二页,整窗加载(覆盖整个工作台)。
|
||||||
|
// 单实例复用——点不同菜单项时重新 load;token 已注入页面 localStorage。
|
||||||
|
auto* projectWebView = new geopro::app::ProjectWebView(sessionToken);
|
||||||
|
centralStack->addWidget(projectWebView); // index 1:项目管理 web 整窗
|
||||||
|
|
||||||
// ── 下方「数据详情」dock:平面图表多 Tab 面板(QGraphicsView,VTK 仅算几何)──
|
// ── 下方「数据详情」dock:平面图表多 Tab 面板(QGraphicsView,VTK 仅算几何)──
|
||||||
// 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。
|
// 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。
|
||||||
auto* detailPanel = new geopro::app::DatasetDetailPanel();
|
auto* detailPanel = new geopro::app::DatasetDetailPanel();
|
||||||
|
|
@ -1320,6 +1500,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
|
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
|
||||||
&geopro::controller::WorkbenchNavController::switchWorkspace);
|
&geopro::controller::WorkbenchNavController::switchWorkspace);
|
||||||
|
|
||||||
|
// 项目管理「直接嵌入」web 页:拼当前项目的嵌入 URL,在中央区整窗加载(切到 web 页)。
|
||||||
|
// space=3 为「项目空间(projectSpace)」固定常量——Excel 所有 projectSpace 页均 space=3,
|
||||||
|
// 与租户/工作空间 id 无关(误用工作空间 snowflake 会被后端拒:space 参数无效)。
|
||||||
|
QObject::connect(
|
||||||
|
topBar, &geopro::app::TopBar::webPageRequested, &window,
|
||||||
|
[projectWebView, centralStack, &nav](const QString& /*title*/, const QString& target) {
|
||||||
|
const QString url =
|
||||||
|
QStringLiteral("http://tenant.geomative.cn/#/embed?space=3&projectId=%1&target=%2")
|
||||||
|
.arg(nav.currentProjectId(), target);
|
||||||
|
projectWebView->load(url);
|
||||||
|
centralStack->setCurrentWidget(projectWebView); // 整窗切到 web 页
|
||||||
|
});
|
||||||
|
// 视图菜单「分析视图」:中央区切回工作台(默认视图)。
|
||||||
|
QObject::connect(topBar, &geopro::app::TopBar::analysisViewRequested, &window,
|
||||||
|
[centralStack, dockManager]() {
|
||||||
|
centralStack->setCurrentWidget(dockManager);
|
||||||
|
});
|
||||||
|
|
||||||
// 切换到「不同项目」时先清空中央区,避免新项目残留旧项目的三栏数据与 VTK 渲染。
|
// 切换到「不同项目」时先清空中央区,避免新项目残留旧项目的三栏数据与 VTK 渲染。
|
||||||
// 仅真正换项目用(delete-refresh 等 switchProject(currentProjectId) 不走此处,避免误清)。
|
// 仅真正换项目用(delete-refresh 等 switchProject(currentProjectId) 不走此处,避免误清)。
|
||||||
auto clearCentral = [sceneCtrl, emptyState, checkedProfiles, checkedAnalysis,
|
auto clearCentral = [sceneCtrl, emptyState, checkedProfiles, checkedAnalysis,
|
||||||
|
|
@ -1920,6 +2118,15 @@ int main(int argc, char* argv[])
|
||||||
// (当前详情链路为同线程 DirectConnection,非严格必需,但作防御性注册,见 spec §5.1)。
|
// (当前详情链路为同线程 DirectConnection,非严格必需,但作防御性注册,见 spec §5.1)。
|
||||||
qRegisterMetaType<geopro::net::ApiResponse>();
|
qRegisterMetaType<geopro::net::ApiResponse>();
|
||||||
|
|
||||||
|
// Qt 标准控件文案中文化:安装 Qt 自带 zh_CN 翻译 → QMessageBox/QDialogButtonBox/QFileDialog/
|
||||||
|
// QColorDialog 等的 OK/Cancel/Yes/No/Save… 全局显示中文。translator 须存活至程序结束(放 main 栈)。
|
||||||
|
QTranslator qtZhTranslator;
|
||||||
|
const QString appTr = QCoreApplication::applicationDirPath() + QStringLiteral("/translations");
|
||||||
|
if (qtZhTranslator.load(QStringLiteral("qtbase_zh_CN"), appTr) || // 部署版(exe 旁)
|
||||||
|
qtZhTranslator.load(QStringLiteral("qtbase_zh_CN"),
|
||||||
|
QLibraryInfo::path(QLibraryInfo::TranslationsPath))) // dev(Qt 安装)
|
||||||
|
app.installTranslator(&qtZhTranslator);
|
||||||
|
|
||||||
// 组织/应用名:QSettings 持久化(dock 布局、登录记忆等)按此定位存储位置。
|
// 组织/应用名:QSettings 持久化(dock 布局、登录记忆等)按此定位存储位置。
|
||||||
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
||||||
|
|
@ -2030,7 +2237,7 @@ int main(int argc, char* argv[])
|
||||||
// 而是给出可见错误提示。否则登录后窗口未显示、进程消失,用户无从排查。
|
// 而是给出可见错误提示。否则登录后窗口未显示、进程消失,用户无从排查。
|
||||||
try {
|
try {
|
||||||
buildWorkbench(*window, repo, projectRepo, datasetRepo, colorTplRepo, cmdRepo, nav,
|
buildWorkbench(*window, repo, projectRepo, datasetRepo, colorTplRepo, cmdRepo, nav,
|
||||||
detailCtrl);
|
detailCtrl, token);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
QMessageBox::critical(
|
QMessageBox::critical(
|
||||||
nullptr, QStringLiteral("启动失败"),
|
nullptr, QStringLiteral("启动失败"),
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,16 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
|
||||||
|
|
||||||
auto* content = new QWidget(scroll);
|
auto* content = new QWidget(scroll);
|
||||||
auto* col = new QVBoxLayout(content);
|
auto* col = new QVBoxLayout(content);
|
||||||
col->setContentsMargins(0, 0, 0, 0);
|
col_ = col;
|
||||||
|
col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶
|
||||||
col->setSpacing(space::kSm);
|
col->setSpacing(space::kSm);
|
||||||
|
|
||||||
for (const CategorySpec& spec : categoryConfigs()) {
|
for (const CategorySpec& spec : categoryConfigs()) {
|
||||||
auto* sec = new CategorySection(spec, dict, content);
|
auto* sec = new CategorySection(spec, dict, content);
|
||||||
sections_[spec.id] = sec;
|
sections_[spec.id] = sec;
|
||||||
|
ordered_.push_back(sec);
|
||||||
|
connect(sec, &CategorySection::collapsedChanged, this,
|
||||||
|
&CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch(向上收)
|
||||||
const std::string segId = spec.id;
|
const std::string segId = spec.id;
|
||||||
connect(sec, &CategorySection::checkedDatasetsChanged, this,
|
connect(sec, &CategorySection::checkedDatasetsChanged, this,
|
||||||
[this, segId](const QStringList& ids) {
|
[this, segId](const QStringList& ids) {
|
||||||
|
|
@ -54,9 +58,22 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
|
||||||
// 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。
|
// 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。
|
||||||
col->addWidget(sec, 1);
|
col->addWidget(sec, 1);
|
||||||
}
|
}
|
||||||
|
// 尾部弹簧(末项):默认 0;全部段折叠时由 relayoutSections 置 1,吸收余量把段头顶到顶部。
|
||||||
|
col->addStretch(0);
|
||||||
scroll->setWidget(content);
|
scroll->setWidget(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CategoryAnalysisTab::relayoutSections() {
|
||||||
|
if (!col_) return;
|
||||||
|
int expanded = 0;
|
||||||
|
for (auto* sec : ordered_)
|
||||||
|
if (sec->isExpanded()) ++expanded;
|
||||||
|
// 展开段 stretch=1(吸收余量、铺满);折叠段 stretch=0(只占段头高,下方不再留空)。
|
||||||
|
for (auto* sec : ordered_) col_->setStretchFactor(sec, sec->isExpanded() ? 1 : 0);
|
||||||
|
// 尾部弹簧:仅当全部折叠时=1(把所有段头顶到顶部);有任一展开段时=0(由展开段吸收余量)。
|
||||||
|
col_->setStretch(col_->count() - 1, expanded == 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
|
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
|
||||||
const auto& cfg = categoryConfigs();
|
const auto& cfg = categoryConfigs();
|
||||||
for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) {
|
for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
class QVBoxLayout;
|
||||||
#include "DatasetCategory.hpp" // CategoryBuckets
|
#include "DatasetCategory.hpp" // CategoryBuckets
|
||||||
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
|
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
|
||||||
#include "repo/RepoTypes.hpp"
|
#include "repo/RepoTypes.hpp"
|
||||||
|
|
@ -45,8 +47,13 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void recomputeCheckedUnion();
|
void recomputeCheckedUnion();
|
||||||
|
// 据各段折叠态重排 stretch:折叠段=0(仅段头高)、展开段=1(吸收余量);全折叠时尾部弹簧吸收→段头顶到顶部。
|
||||||
|
// 仅在面板不产生滚动条(内容short于视口)时有可见效果——正是用户反馈的"折叠后停在原位中间"场景。
|
||||||
|
void relayoutSections();
|
||||||
|
|
||||||
std::map<std::string, CategorySection*> sections_;
|
std::map<std::string, CategorySection*> sections_;
|
||||||
|
std::vector<CategorySection*> ordered_; // 按 categoryConfigs 顺序(relayout 遍历用)
|
||||||
|
QVBoxLayout* col_ = nullptr; // 段堆叠布局(末项=尾部弹簧)
|
||||||
std::map<std::string, QStringList> checkedBySeg_; // 各段当前勾选(合并成并集)
|
std::map<std::string, QStringList> checkedBySeg_; // 各段当前勾选(合并成并集)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,24 +33,50 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
root->setContentsMargins(0, 0, 0, 0);
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
root->setSpacing(0);
|
root->setSpacing(0);
|
||||||
|
|
||||||
// 数据类型标题行(spec §7):折叠箭头+标题(左) | 「+新增三维体」(右,仅反演类,在标题行而非筛选行)。
|
// 数据类型段头(可折叠,规范§4.3/§6):chevron + 标题(title 字号·半粗) |「+ 新增三维体」(右,仅反演类)。
|
||||||
|
// 段头加浅底 + 底分隔线作视觉分段;去原生小三角(难看)→chevron 文本前缀,随主题/hover 变色。
|
||||||
auto* headerRow = new QWidget(this);
|
auto* headerRow = new QWidget(this);
|
||||||
|
headerRow->setObjectName(QStringLiteral("secHeader"));
|
||||||
|
applyTokenizedStyleSheet(headerRow,
|
||||||
|
QStringLiteral("QWidget#secHeader{background:{{bg/panel-subtle}};"
|
||||||
|
"border-bottom:1px solid {{divider}};}"));
|
||||||
auto* hl = new QHBoxLayout(headerRow);
|
auto* hl = new QHBoxLayout(headerRow);
|
||||||
hl->setContentsMargins(space::kSm, 0, space::kSm, 0);
|
hl->setContentsMargins(space::kMd, space::kSm, space::kSm, space::kSm);
|
||||||
hl->setSpacing(space::kSm);
|
hl->setSpacing(space::kSm);
|
||||||
header_ = new QToolButton(headerRow);
|
header_ = new QToolButton(headerRow);
|
||||||
header_->setText(QString::fromStdString(spec_.title));
|
|
||||||
header_->setCheckable(true);
|
header_->setCheckable(true);
|
||||||
header_->setChecked(true);
|
header_->setChecked(true);
|
||||||
header_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
header_->setArrowType(Qt::NoArrow);
|
||||||
header_->setArrowType(Qt::DownArrow);
|
header_->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||||
header_->setAutoRaise(true);
|
header_->setCursor(Qt::PointingHandCursor);
|
||||||
|
applyTokenizedStyleSheet(
|
||||||
|
header_, QStringLiteral("QToolButton{border:none;background:transparent;padding:0;"
|
||||||
|
"font-size:%1px;font-weight:%2;color:{{text/primary}};}"
|
||||||
|
"QToolButton:hover{color:{{accent/primary}};}")
|
||||||
|
.arg(scaledPx(type::kTitle))
|
||||||
|
.arg(type::kWeightSemibold));
|
||||||
|
auto syncHeader = [this] {
|
||||||
|
header_->setText((header_->isChecked() ? QStringLiteral("▾ ") : QStringLiteral("▸ "))
|
||||||
|
+ QString::fromStdString(spec_.title));
|
||||||
|
};
|
||||||
|
syncHeader();
|
||||||
hl->addWidget(header_);
|
hl->addWidget(header_);
|
||||||
hl->addStretch(1);
|
hl->addStretch(1);
|
||||||
if (spec_.canGenerateVolume) {
|
if (spec_.canGenerateVolume) {
|
||||||
auto* gen = new QToolButton(headerRow);
|
auto* gen = new QToolButton(headerRow);
|
||||||
gen->setText(QStringLiteral("+ 新增三维体"));
|
gen->setText(QStringLiteral("+ 新增三维体"));
|
||||||
gen->setAutoRaise(true);
|
gen->setCursor(Qt::PointingHandCursor);
|
||||||
|
// 次级强调按钮(规范§6.7):描边 accent + accent 文字,hover 浅强调底;非裸文字。
|
||||||
|
applyTokenizedStyleSheet(
|
||||||
|
gen, QStringLiteral(
|
||||||
|
"QToolButton{border:1px solid {{accent/primary}};border-radius:%1px;"
|
||||||
|
"color:{{accent/primary}};background:transparent;padding:%2px %3px;font-size:%4px;}"
|
||||||
|
"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)));
|
||||||
connect(gen, &QToolButton::clicked, this, [this] {
|
connect(gen, &QToolButton::clicked, this, [this] {
|
||||||
emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds());
|
emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds());
|
||||||
});
|
});
|
||||||
|
|
@ -65,6 +91,7 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
|
|
||||||
// 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。
|
// 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。
|
||||||
auto* filterRow = new QHBoxLayout();
|
auto* filterRow = new QHBoxLayout();
|
||||||
|
filterRow->setSpacing(space::kSm);
|
||||||
dateRange_ = new DateRangeEdit(body_);
|
dateRange_ = new DateRangeEdit(body_);
|
||||||
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
|
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
|
||||||
filterRow->addWidget(dateRange_, 1);
|
filterRow->addWidget(dateRange_, 1);
|
||||||
|
|
@ -115,12 +142,15 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
|
||||||
|
|
||||||
root->addWidget(body_, 1);
|
root->addWidget(body_, 1);
|
||||||
|
|
||||||
connect(header_, &QToolButton::toggled, this, [this](bool on) {
|
connect(header_, &QToolButton::toggled, this, [this, syncHeader](bool on) {
|
||||||
body_->setVisible(on);
|
body_->setVisible(on);
|
||||||
header_->setArrowType(on ? Qt::DownArrow : Qt::RightArrow);
|
syncHeader(); // ▾(展开)/▸(折叠) 切换
|
||||||
|
emit collapsedChanged(); // 外层据此把折叠段 stretch 归 0、展开段吸收余量 → 折叠向上收
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CategorySection::isExpanded() const { return header_ && header_->isChecked(); }
|
||||||
|
|
||||||
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 接入真实结构后据此构建。
|
||||||
}
|
}
|
||||||
|
|
@ -313,14 +343,12 @@ void CategorySection::showContextMenu(const QPoint& pos) {
|
||||||
sl->addAction(QStringLiteral("前后"), this, [this, id] { emit sliceRequested(SliceAxis::FrontBack, id); });
|
sl->addAction(QStringLiteral("前后"), this, [this, id] { emit sliceRequested(SliceAxis::FrontBack, id); });
|
||||||
sl->addAction(QStringLiteral("左右"), this, [this, id] { emit sliceRequested(SliceAxis::LeftRight, id); });
|
sl->addAction(QStringLiteral("左右"), this, [this, id] { emit sliceRequested(SliceAxis::LeftRight, id); });
|
||||||
sl->addAction(QStringLiteral("任意"), this, [this, id] { emit sliceRequested(SliceAxis::Oblique, id); });
|
sl->addAction(QStringLiteral("任意"), this, [this, id] { emit sliceRequested(SliceAxis::Oblique, id); });
|
||||||
menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); });
|
menu.addAction(QStringLiteral("色阶"), this, [this, id] { emit colorScaleRequested(id); });
|
||||||
} else if (ddCode == QStringLiteral("dd_slice")) { // 切片
|
} else if (ddCode == QStringLiteral("dd_slice")) { // 切片(列表中均为已保存=定稿锁定,无保存/另存)
|
||||||
menu.addAction(QStringLiteral("保存位姿"), this, [this, id] { emit sliceSaveRequested(id); });
|
|
||||||
menu.addAction(QStringLiteral("另存为…"), this, [this, id] { emit sliceSaveAsRequested(id); });
|
|
||||||
QMenu* ex = menu.addMenu(QStringLiteral("导出"));
|
QMenu* ex = menu.addMenu(QStringLiteral("导出"));
|
||||||
ex->addAction(QStringLiteral("图片"), this, [this, id] { emit sliceExportImageRequested(id); });
|
ex->addAction(QStringLiteral("图片"), this, [this, id] { emit sliceExportImageRequested(id); });
|
||||||
ex->addAction(QStringLiteral("dat"), this, [this, id] { emit sliceExportDatRequested(id); });
|
ex->addAction(QStringLiteral("dat"), this, [this, id] { emit sliceExportDatRequested(id); });
|
||||||
menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); });
|
menu.addAction(QStringLiteral("色阶"), this, [this, id] { emit colorScaleRequested(id); });
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); });
|
menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); });
|
||||||
} else if (ddCode == QStringLiteral("dd_anomaly")) { // 异常
|
} else if (ddCode == QStringLiteral("dd_anomaly")) { // 异常
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,11 @@ public:
|
||||||
QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds(异常显隐同步用)
|
QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds(异常显隐同步用)
|
||||||
void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉
|
void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉
|
||||||
const CategorySpec& spec() const { return spec_; }
|
const CategorySpec& spec() const { return spec_; }
|
||||||
|
bool isExpanded() const; // 段头展开态(供外层按折叠状态重分配 stretch,实现"折叠向上收")
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染
|
void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染
|
||||||
|
void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch
|
||||||
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」
|
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」
|
||||||
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情
|
void detailRequested(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); // 右键删除(切片/异常)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
#include "panels/columns/Column2DDataset.hpp"
|
#include "panels/columns/Column2DDataset.hpp"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
||||||
#include "EmptyAwareComboBox.hpp"
|
#include "EmptyAwareComboBox.hpp"
|
||||||
|
|
@ -69,6 +72,7 @@ Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) {
|
||||||
list_ = new QTreeWidget();
|
list_ = new QTreeWidget();
|
||||||
list_->setHeaderHidden(true);
|
list_->setHeaderHidden(true);
|
||||||
list_->setRootIsDecorated(true);
|
list_->setRootIsDecorated(true);
|
||||||
|
list_->setSelectionMode(QAbstractItemView::ExtendedSelection); // 多选行(与 VTK 多选拖动联动)
|
||||||
applyDatasetCardDelegate(list_);
|
applyDatasetCardDelegate(list_);
|
||||||
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
||||||
QStringList ids;
|
QStringList ids;
|
||||||
|
|
@ -78,19 +82,36 @@ Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) {
|
||||||
}
|
}
|
||||||
emit checkedDatasetsChanged(ids);
|
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);
|
root->addWidget(list_, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Column2DDataset::setDatasets(const std::vector<geopro::data::DsRow>& rows) {
|
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_);
|
QSignalBlocker blocker(list_);
|
||||||
populateDatasetList(list_, rows, /*append=*/false);
|
populateDatasetList(list_, rows, /*append=*/false);
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
||||||
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
|
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
|
||||||
(*it)->setCheckState(0, Qt::Unchecked);
|
const std::string id = (*it)->data(0, kDsIdRole).toString().toStdString();
|
||||||
|
// 复原勾选:仍存在的曾勾选项保持勾选;新项默认不勾。
|
||||||
|
(*it)->setCheckState(0, wasChecked.count(id) ? Qt::Checked : Qt::Unchecked);
|
||||||
}
|
}
|
||||||
} // blocker released here
|
} // blocker released here
|
||||||
// 填充后统一发一次(新载入必为空选):清掉上一次的渲染勾选
|
// 上抛复原后的勾选集(保持渲染,不再清空 → 控制器据 diff 增量保留已渲染足迹,集合不变则不增删)。
|
||||||
QStringList ids;
|
QStringList ids;
|
||||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||||||
if ((*it)->checkState(0) == Qt::Checked)
|
if ((*it)->checkState(0) == Qt::Checked)
|
||||||
|
|
@ -98,4 +119,11 @@ void Column2DDataset::setDatasets(const std::vector<geopro::data::DsRow>& rows)
|
||||||
emit checkedDatasetsChanged(ids);
|
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
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,15 @@ class Column2DDataset : public QWidget {
|
||||||
public:
|
public:
|
||||||
explicit Column2DDataset(QWidget* parent = nullptr);
|
explicit Column2DDataset(QWidget* parent = nullptr);
|
||||||
void setDatasets(const std::vector<geopro::data::DsRow>& rows);
|
void setDatasets(const std::vector<geopro::data::DsRow>& rows);
|
||||||
|
// VTK→列表 选择联动:按 dsId 选中对应行(高亮),内部屏蔽信号避免回环。
|
||||||
|
void setSelectedDsIds(const QStringList& dsIds);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void basemapChanged(int index); // 0 天地图 / 1 Google / 2 隐藏
|
void basemapChanged(int index); // 0 天地图 / 1 Google / 2 隐藏
|
||||||
void view2DModeChanged(int index); // 0 关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义
|
void view2DModeChanged(int index); // 0 关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义
|
||||||
void customZChanged(double z); // 世界绝对高程(米),向上为正
|
void customZChanged(double z); // 世界绝对高程(米),向上为正
|
||||||
void checkedDatasetsChanged(const QStringList& dsIds);
|
void checkedDatasetsChanged(const QStringList& dsIds); // 勾选(渲染开关)变化
|
||||||
|
void selectedDatasetsChanged(const QStringList& dsIds); // 行选中(高亮联动)变化,非勾选
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTreeWidget* list_ = nullptr;
|
QTreeWidget* list_ = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary
|
||||||
tabs->addTab(analysisTab_, QStringLiteral("三维分析"));
|
tabs->addTab(analysisTab_, QStringLiteral("三维分析"));
|
||||||
tabs->addTab(col2D_, QStringLiteral("二维分析"));
|
tabs->addTab(col2D_, QStringLiteral("二维分析"));
|
||||||
tabs->tabBar()->setUsesScrollButtons(false); // 永不出左右滚动箭头(两 tab 必能平铺)
|
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,作按钮文字会触发
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ public:
|
||||||
Column2DDataset* col2D() const { return col2D_; }
|
Column2DDataset* col2D() const { return col2D_; }
|
||||||
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
|
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
// 切换「三维分析 / 二维分析」tab:is2D=true 进入二维分析。上层据此切相机+翻维度可见标志。
|
||||||
|
void analysisModeChanged(bool is2D);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void toggleCollapsed();
|
void toggleCollapsed();
|
||||||
void expand(); // 强制展开(进入全屏时确保三栏可见)
|
void expand(); // 强制展开(进入全屏时确保三栏可见)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#include "panels/web/ProjectWebView.hpp"
|
||||||
|
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWebEnginePage>
|
||||||
|
#include <QWebEngineScript>
|
||||||
|
#include <QWebEngineScriptCollection>
|
||||||
|
#include <QWebEngineView>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 把字符串转成安全的 JS 字面量(带引号、转义),用于拼进注入脚本。
|
||||||
|
QString jsStringLiteral(const QString& s) {
|
||||||
|
// QJsonValue::toJson 不直接给单值字符串;手工转义足够(token 仅含 base64/空格)。
|
||||||
|
QString out;
|
||||||
|
out.reserve(s.size() + 2);
|
||||||
|
out += QLatin1Char('"');
|
||||||
|
for (const QChar c : s) {
|
||||||
|
switch (c.unicode()) {
|
||||||
|
case '\\': out += QStringLiteral("\\\\"); break;
|
||||||
|
case '"': out += QStringLiteral("\\\""); break;
|
||||||
|
case '\n': out += QStringLiteral("\\n"); break;
|
||||||
|
case '\r': out += QStringLiteral("\\r"); break;
|
||||||
|
case '\t': out += QStringLiteral("\\t"); break;
|
||||||
|
default: out += c; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += QLatin1Char('"');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ProjectWebView::ProjectWebView(const QString& token, QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QVBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
|
||||||
|
view_ = new QWebEngineView(this);
|
||||||
|
lay->addWidget(view_, 1);
|
||||||
|
|
||||||
|
// token 注入:DocumentCreation 阶段把登录 token 写入 localStorage["token"],
|
||||||
|
// 早于嵌入页 SPA 启动脚本,保证其读取鉴权时已就绪。每次 load 都会重新执行。
|
||||||
|
if (!token.isEmpty()) {
|
||||||
|
QWebEngineScript script;
|
||||||
|
script.setName(QStringLiteral("inject-geopro-token"));
|
||||||
|
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
|
||||||
|
script.setWorldId(QWebEngineScript::MainWorld);
|
||||||
|
script.setRunsOnSubFrames(true);
|
||||||
|
script.setSourceCode(
|
||||||
|
QStringLiteral("try{localStorage.setItem('token', %1);}catch(e){}")
|
||||||
|
.arg(jsStringLiteral(token)));
|
||||||
|
view_->page()->scripts().insert(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectWebView::load(const QString& url) {
|
||||||
|
view_->load(QUrl(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QWebEngineView;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 项目管理 webview 宿主:内嵌 QWebEngineView,承载需「直接嵌入」的 web 管理页
|
||||||
|
// (在线监测 / 工具组件 / 批量导出 / 告警管理)。
|
||||||
|
// 构造期注入 DocumentCreation 脚本,把登录 token 写入页面 localStorage["token"],
|
||||||
|
// 早于页面自身脚本执行,确保 web 端读取鉴权时已就绪。
|
||||||
|
class ProjectWebView : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ProjectWebView(const QString& token, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// 加载嵌入页(完整 URL,含 #/embed?space=..&projectId=..&target=..)。
|
||||||
|
void load(const QString& url);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWebEngineView* view_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -46,7 +46,8 @@ void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
|
||||||
// 取景意图按「场景是否已有数据到场过」判定,而非 checkedDs_ 是否空——否则连续快速勾选第二个
|
// 取景意图按「场景是否已有数据到场过」判定,而非 checkedDs_ 是否空——否则连续快速勾选第二个
|
||||||
// ds 时 checkedDs_ 已非空但首批尚未到场,会被误清取景意图,相机不对准数据 → 看似不渲染。
|
// ds 时 checkedDs_ 已非空但首批尚未到场,会被误清取景意图,相机不对准数据 → 看似不渲染。
|
||||||
fitOnArrival_ = !hadArrivedData_;
|
fitOnArrival_ = !hadArrivedData_;
|
||||||
if (checkedDs_.empty()) hadArrivedData_ = false; // 全取消 → 下批到场重新取景
|
// 仅当 3D 与 2D 都空才复位取景基线:否则取消全部 3D 但仍有 2D 足迹时,下个 3D 勾选会误跳取景。
|
||||||
|
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
|
||||||
|
|
||||||
const unsigned long long gen = rebuildGeneration_; // 不自增:并发增量互不作废
|
const unsigned long long gen = rebuildGeneration_; // 不自增:并发增量互不作废
|
||||||
for (const auto& id : checkedDs_)
|
for (const auto& id : checkedDs_)
|
||||||
|
|
@ -63,14 +64,16 @@ void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) {
|
||||||
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff(不全量重建,不打断 3D 帘面/体)。
|
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff(不全量重建,不打断 3D 帘面/体)。
|
||||||
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
|
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
|
||||||
const std::set<std::string> newSet(newDs.begin(), newDs.end());
|
const std::set<std::string> newSet(newDs.begin(), newDs.end());
|
||||||
// 此前空场景(无 3D 数据且无 2D 足迹) → 首批足迹到场自动取景;否则增量追加保持相机不跳。
|
|
||||||
const bool wasEmpty = checkedDs_.empty() && checked2dDs_.empty();
|
|
||||||
|
|
||||||
for (const auto& id : checked2dDs_)
|
for (const auto& id : checked2dDs_)
|
||||||
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
|
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
|
||||||
|
|
||||||
checked2dDs_ = std::move(newDs);
|
checked2dDs_ = std::move(newDs);
|
||||||
fitOnArrival_ = wasEmpty; // 首批足迹(空场景)取景;否则保持当前相机不跳
|
// 取景基线与 3D 路径统一用 hadArrivedData_(而非"两栏皆空"):否则二维分析下若已有隐藏的 3D 数据,
|
||||||
|
// 勾选首条足迹会因 wasEmpty=false 而不取景 → 足迹落在视野外。切 tab 时 onAnalysisModeChanged 已按
|
||||||
|
// 目标维度是否有数据重置该基线,故此处首条可见维度数据能正确取景。
|
||||||
|
fitOnArrival_ = !hadArrivedData_;
|
||||||
|
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
|
||||||
|
|
||||||
// 足迹画进 View3D 场景;mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
|
// 足迹画进 View3D 场景;mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
|
||||||
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
|
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
|
||||||
|
|
@ -210,6 +213,14 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ public slots:
|
||||||
// 二维足迹摆放高度(mode:0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义;customZ 仅 mode=4 用)。
|
// 二维足迹摆放高度(mode:0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义;customZ 仅 mode=4 用)。
|
||||||
void set2DPlacement(int mode, double customZ);
|
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);
|
||||||
void rebuild(); // 主题切换等外部触发的重渲染
|
void rebuild(); // 主题切换等外部触发的重渲染
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,14 @@ VolumeGrid builtI16ToVolumeGrid(const geopro::core::BuiltI16& built) {
|
||||||
}
|
}
|
||||||
|
|
||||||
VolumeGrid createGprVolumeGrid(const std::string& lineDir,
|
VolumeGrid createGprVolumeGrid(const std::string& lineDir,
|
||||||
const std::string& linePrefix, int coarse) {
|
const std::string& linePrefix, int coarse,
|
||||||
|
double targetDy) {
|
||||||
// 走 P1/P2 链(io::gpr)得处理后 int16 量化体 → 反量化为 app 的 float 体。
|
// 走 P1/P2 链(io::gpr)得处理后 int16 量化体 → 反量化为 app 的 float 体。
|
||||||
// metricsOut 传 nullptr:repository 只产数据,度量留给 gpr_poc CLI。
|
// metricsOut 传 nullptr:repository 只产数据,度量留给 gpr_poc CLI。
|
||||||
|
// targetDy 透传 → 默认走线内通道插值(2.5cm 网格),app 渲染链即得密 Y 体。
|
||||||
const geopro::core::BuiltI16 built =
|
const geopro::core::BuiltI16 built =
|
||||||
geopro::io::gpr::buildLineVolumeFromGpr3dv(lineDir, linePrefix,
|
geopro::io::gpr::buildLineVolumeFromGpr3dv(
|
||||||
/*metricsOut=*/nullptr, coarse);
|
lineDir, linePrefix, /*metricsOut=*/nullptr, coarse, targetDy);
|
||||||
return builtI16ToVolumeGrid(built);
|
return builtI16ToVolumeGrid(built);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,12 @@ VolumeGrid builtI16ToVolumeGrid(const geopro::core::BuiltI16& built);
|
||||||
// lineDir/linePrefix 同 gpr3dv-smoke / build-line(如 "D:/Downloads/明星路", "明星路_001")。
|
// lineDir/linePrefix 同 gpr3dv-smoke / build-line(如 "D:/Downloads/明星路", "明星路_001")。
|
||||||
// coarse(下采样因子,≥1):沿测线(道/X 轴)每 coarse 道取 1,省内存;横向/深度保全分辨率。
|
// coarse(下采样因子,≥1):沿测线(道/X 轴)每 coarse 道取 1,省内存;横向/深度保全分辨率。
|
||||||
// 稠密 VolumeGrid 全内存,长线需较大 coarse 控内存(默认 4 = build-line POC 档)。
|
// 稠密 VolumeGrid 全内存,长线需较大 coarse 控内存(默认 4 = build-line POC 档)。
|
||||||
|
// targetDy(米,>0 启用):线内【通道间插值】目标横向间距(读真实道偏移规则化,不跨线)。
|
||||||
|
// 默认 0.025(2.5cm);0=不插值(Y=原通道数)。详见 io::gpr::buildLineVolumeFromGpr3dv。
|
||||||
// 失败(加载失败/立方体为空)→ 抛 std::runtime_error(由 io::gpr 链抛出,原样透传)。
|
// 失败(加载失败/立方体为空)→ 抛 std::runtime_error(由 io::gpr 链抛出,原样透传)。
|
||||||
VolumeGrid createGprVolumeGrid(const std::string& lineDir,
|
VolumeGrid createGprVolumeGrid(const std::string& lineDir,
|
||||||
const std::string& linePrefix, int coarse = 4);
|
const std::string& linePrefix, int coarse = 4,
|
||||||
|
double targetDy = 0.025);
|
||||||
|
|
||||||
} // namespace geopro::data
|
} // namespace geopro::data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include "algo/VolumeBuilder.hpp" // core::PointSet / BuiltVolume / buildVolume(含 Field.hpp)
|
#include "algo/VolumeBuilder.hpp" // core::PointSet / BuiltVolume / buildVolume(含 Field.hpp)
|
||||||
#include "api/DatasetLoadHandles.hpp"
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
#include "GprVolumeRepository.hpp" // createGprVolumeGrid(§6 接入:GPR 体直产)
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/detail/DetailPayloads.hpp"
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
#include "repo/IAsyncDatasetRepository.hpp"
|
#include "repo/IAsyncDatasetRepository.hpp"
|
||||||
|
|
@ -38,11 +39,9 @@ DsDimension Api3dRepository::dimensionOf(const DsRow& ds) const {
|
||||||
return DsDimension::Dim3D;
|
return DsDimension::Dim3D;
|
||||||
}
|
}
|
||||||
if (c == "dd_slice") return DsDimension::Analysis3D;
|
if (c == "dd_slice") return DsDimension::Analysis3D;
|
||||||
// 足迹型(测线/各类轨迹) → 二维数据集:地面 lat/lon 序列,平铺进地图(spec §4.1/§4.2)。
|
// 足迹型 → 二维数据集:地面 lat/lon 序列,平铺进地图。dd_trajectory_data = 统一通用轨迹
|
||||||
if (c == "dd_trajectory_data" || c == "dd_transient_electromagnetic_trajectory_data" ||
|
// (数据字典 DD0623「保留」,已并入 dd_radar_rtk_trajectory);瞬变电磁/雷达通道/RTK 轨迹字典均「删除」。
|
||||||
c == "dd_radar_channel_trajectory" || c == "dd_radar_rtk_trajectory") {
|
if (c == "dd_trajectory_data") return DsDimension::Dim2D;
|
||||||
return DsDimension::Dim2D;
|
|
||||||
}
|
|
||||||
return DsDimension::Other;
|
return DsDimension::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +120,29 @@ std::string Api3dRepository::createVolume(const VoxelGenerateRequest& req) {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Api3dRepository::createGprVolume(const std::string& lineDir,
|
||||||
|
const std::string& linePrefix,
|
||||||
|
const std::string& name, int coarse) {
|
||||||
|
// 走 io::gpr 逐线管线(含线内通道插值)直接产体(抛异常透传给调用方)。
|
||||||
|
VolumeGrid grid = geopro::data::createGprVolumeGrid(lineDir, linePrefix, coarse);
|
||||||
|
// 简易灰度色阶(负→暗、零→灰、正→亮)覆盖体值域,使体素渲染可见。
|
||||||
|
core::ColorScale scale;
|
||||||
|
const double mid = 0.5 * (grid.vmin + grid.vmax);
|
||||||
|
scale.addStop(grid.vmin, core::Rgba{20, 24, 40, 255});
|
||||||
|
scale.addStop(mid, core::Rgba{140, 140, 150, 255});
|
||||||
|
scale.addStop(grid.vmax, core::Rgba{235, 232, 220, 255});
|
||||||
|
|
||||||
|
const std::string id = "vol-" + std::to_string(++volumeCounter_);
|
||||||
|
StoredVolume sv;
|
||||||
|
sv.name = name;
|
||||||
|
sv.createTime =
|
||||||
|
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString();
|
||||||
|
sv.cachedGrid = std::move(grid); // 预填 → loadVolume 直接命中渲染(不走 mock IDW)
|
||||||
|
sv.cachedScale = scale;
|
||||||
|
volumes_[id] = std::move(sv);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
const VoxelGenerateRequest* Api3dRepository::lastVoxelRequest(const std::string& dsId) const {
|
const VoxelGenerateRequest* Api3dRepository::lastVoxelRequest(const std::string& dsId) const {
|
||||||
const auto it = volumes_.find(dsId);
|
const auto it = volumes_.find(dsId);
|
||||||
return (it != volumes_.end() && it->second.request) ? &*it->second.request : nullptr;
|
return (it != volumes_.end() && it->second.request) ? &*it->second.request : nullptr;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@ public:
|
||||||
std::string createVolume(VolumeBuildParams params, const std::string& name);
|
std::string createVolume(VolumeBuildParams params, const std::string& name);
|
||||||
// 请求体形态创建:组装真实 VoxelGenerateRequest → 派生 params 存储 + 打印请求体 JSON(供后端联调)。
|
// 请求体形态创建:组装真实 VoxelGenerateRequest → 派生 params 存储 + 打印请求体 JSON(供后端联调)。
|
||||||
std::string createVolume(const VoxelGenerateRequest& req);
|
std::string createVolume(const VoxelGenerateRequest& req);
|
||||||
|
// GPR 三维体:走 io::gpr 逐线管线(含线内通道插值,§1)直接产体并【预填 cachedGrid】,
|
||||||
|
// 注册为 dd_voxel 体 → 自动进 volumeRows/三级树,loadVolume 直接命中渲染(不走 mock IDW)。
|
||||||
|
// lineDir/linePrefix 同 build-line(如 "D:/Downloads/明星路","明星路_010");coarse 控内存。
|
||||||
|
// 返回新 dsId;失败抛 std::runtime_error(加载/立方体空,由 io::gpr 链透传)。
|
||||||
|
std::string createGprVolume(const std::string& lineDir, const std::string& linePrefix,
|
||||||
|
const std::string& name, int coarse = 8);
|
||||||
// 取回某三维体组装的请求体(测试/联调);非本 repo 创建或无 request 时返回 nullptr。
|
// 取回某三维体组装的请求体(测试/联调);非本 repo 创建或无 request 时返回 nullptr。
|
||||||
const VoxelGenerateRequest* lastVoxelRequest(const std::string& dsId) const;
|
const VoxelGenerateRequest* lastVoxelRequest(const std::string& dsId) const;
|
||||||
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维分析栏合并注入(每次 setDatasets 追加)。
|
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维分析栏合并注入(每次 setDatasets 追加)。
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,13 @@ void ApiDatasetCommandRepository::listExceptionTypes(
|
||||||
std::move(cb));
|
std::move(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ApiDatasetCommandRepository::getExceptionTypeDetail(
|
||||||
|
const QString& exceptionTypeId, std::function<void(bool, QJsonObject, QString)> cb) {
|
||||||
|
wireObject(api_.getAsync(QStringLiteral("/business/exceptionType/getDetail/%1")
|
||||||
|
.arg(enc(exceptionTypeId))),
|
||||||
|
std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
void ApiDatasetCommandRepository::listArrayTypes(
|
void ApiDatasetCommandRepository::listArrayTypes(
|
||||||
std::function<void(bool, QJsonArray, QString)> cb) {
|
std::function<void(bool, QJsonArray, QString)> cb) {
|
||||||
wireArray(api_.getAsync(QStringLiteral("/business/script/arrayTypeList")), std::move(cb));
|
wireArray(api_.getAsync(QStringLiteral("/business/script/arrayTypeList")), std::move(cb));
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ public:
|
||||||
void listExceptionTypes(
|
void listExceptionTypes(
|
||||||
const QString& projectId, const QString& remarkSourceType,
|
const QString& projectId, const QString& remarkSourceType,
|
||||||
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
|
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
|
||||||
|
void getExceptionTypeDetail(
|
||||||
|
const QString& exceptionTypeId,
|
||||||
|
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
|
||||||
void listArrayTypes(std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
|
void listArrayTypes(std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
|
||||||
void getExceptionName(
|
void getExceptionName(
|
||||||
const QString& exceptionTypeId, const QString& remarkSourceId,
|
const QString& exceptionTypeId, const QString& remarkSourceId,
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,13 @@ public:
|
||||||
const QString& projectId, const QString& remarkSourceType,
|
const QString& projectId, const QString& remarkSourceType,
|
||||||
std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
|
std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
|
||||||
|
|
||||||
|
// 异常类型详情:GET /business/exceptionType/getDetail/{id} → data{...legend...}。
|
||||||
|
// data.legend = {polylineColor/Width/Shape, pointColor/Size/Shape, polygonFillColor,...},
|
||||||
|
// 供创建异常时按平台类型样式渲染(与平台一致)。回调 data = 整个 data 对象。
|
||||||
|
virtual void getExceptionTypeDetail(
|
||||||
|
const QString& exceptionTypeId,
|
||||||
|
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
|
||||||
|
|
||||||
// 装置类型枚举:GET /business/script/arrayTypeList → [{itemValue,name}](电阻率/视电阻率段装置筛选用)。
|
// 装置类型枚举:GET /business/script/arrayTypeList → [{itemValue,name}](电阻率/视电阻率段装置筛选用)。
|
||||||
virtual void listArrayTypes(std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
|
virtual void listArrayTypes(std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,9 @@ DsDimension LocalSample3dRepository::dimensionOf(const DsRow& ds) const {
|
||||||
}
|
}
|
||||||
// 切片:三维分析栏。
|
// 切片:三维分析栏。
|
||||||
if (c == "dd_slice") return DsDimension::Analysis3D;
|
if (c == "dd_slice") return DsDimension::Analysis3D;
|
||||||
// 足迹型(测线/各类轨迹):二维数据集(与 Api3dRepository 同口径)。
|
// 足迹型 → 二维数据集。dd_trajectory_data = 统一通用轨迹(数据字典 DD0623「保留」,已并入
|
||||||
if (c == "dd_trajectory_data" || c == "dd_transient_electromagnetic_trajectory_data" ||
|
// dd_radar_rtk_trajectory);瞬变电磁/雷达通道/RTK 等轨迹型字典均标「删除」,不再单列。
|
||||||
c == "dd_radar_channel_trajectory" || c == "dd_radar_rtk_trajectory") {
|
if (c == "dd_trajectory_data") return DsDimension::Dim2D;
|
||||||
return DsDimension::Dim2D;
|
|
||||||
}
|
|
||||||
return DsDimension::Other;
|
return DsDimension::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@
|
||||||
#include "RadarProcessor.h"
|
#include "RadarProcessor.h"
|
||||||
|
|
||||||
#include "core/model/ScalarVolumeI16.hpp"
|
#include "core/model/ScalarVolumeI16.hpp"
|
||||||
|
#include "io/gpr/GprGeometry.hpp" // planChannelInterpolation
|
||||||
|
|
||||||
namespace geopro::io::gpr {
|
namespace geopro::io::gpr {
|
||||||
|
|
||||||
|
|
@ -68,7 +70,7 @@ double nowMs(std::chrono::steady_clock::time_point t0) {
|
||||||
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
||||||
const std::string& linePrefix,
|
const std::string& linePrefix,
|
||||||
BridgeMetrics* metricsOut,
|
BridgeMetrics* metricsOut,
|
||||||
int coarse) {
|
int coarse, double targetDy) {
|
||||||
const int stride = coarse > 1 ? coarse : 1; // 沿测线下采样步长(≥1)
|
const int stride = coarse > 1 ? coarse : 1; // 沿测线下采样步长(≥1)
|
||||||
const QString dir = QString::fromLocal8Bit(lineDir.c_str());
|
const QString dir = QString::fromLocal8Bit(lineDir.c_str());
|
||||||
const QString base = QString::fromLocal8Bit(linePrefix.c_str());
|
const QString base = QString::fromLocal8Bit(linePrefix.c_str());
|
||||||
|
|
@ -108,9 +110,25 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
||||||
// 下采样后输出道数(向上取整保留末道附近):nxOut = ceil(traces/stride)。
|
// 下采样后输出道数(向上取整保留末道附近):nxOut = ceil(traces/stride)。
|
||||||
const int nxOut = (traces + stride - 1) / stride;
|
const int nxOut = (traces + stride - 1) / stride;
|
||||||
const int nx = nxOut; // X=道(沿测线,已按 stride 下采样)
|
const int nx = nxOut; // X=道(沿测线,已按 stride 下采样)
|
||||||
const int ny = channels; // Y=通道(横向)
|
|
||||||
const int nz = samples; // Z=样本(深度)
|
const int nz = samples; // Z=样本(深度)
|
||||||
|
|
||||||
|
// §1 线内通道插值:读各通道真实横向偏移(header.chXOffsets) → 规则网格化 Y 到 targetDy。
|
||||||
|
// 绝不跨线;间距/通道数从数据来,不假设。退路(无偏移/未启用)= 逐通道 identity。
|
||||||
|
std::vector<double> latOff;
|
||||||
|
const auto& chx = processed.header.chXOffsets;
|
||||||
|
if (chx.size() == channels)
|
||||||
|
for (int c = 0; c < channels; ++c)
|
||||||
|
latOff.push_back(static_cast<double>(chx[c]));
|
||||||
|
std::vector<geopro::io::gpr::ChannelInterpRow> rows;
|
||||||
|
bool interpolated = false;
|
||||||
|
if (static_cast<int>(latOff.size()) == channels && targetDy > 0.0) {
|
||||||
|
rows = planChannelInterpolation(latOff, targetDy);
|
||||||
|
interpolated = (static_cast<int>(rows.size()) != channels);
|
||||||
|
}
|
||||||
|
if (rows.empty())
|
||||||
|
for (int c = 0; c < channels; ++c) rows.push_back({c, c, 0.0});
|
||||||
|
const int ny = static_cast<int>(rows.size()); // Y=通道(横向,可能已插值加密)
|
||||||
|
|
||||||
// 3) 扫处理后值域 → Quant(offset=中点,防溢出)。
|
// 3) 扫处理后值域 → Quant(offset=中点,防溢出)。
|
||||||
const auto tFill = std::chrono::steady_clock::now();
|
const auto tFill = std::chrono::steady_clock::now();
|
||||||
short rawMin = std::numeric_limits<short>::max();
|
short rawMin = std::numeric_limits<short>::max();
|
||||||
|
|
@ -138,20 +156,26 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
||||||
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
|
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
|
||||||
quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出
|
quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出
|
||||||
|
|
||||||
// 4) 逐 (ch,trace,sample) 填体。GPR 立方体为稠密体(每体素有值),无空洞 → 不置 kBlank。
|
// 4) 逐 (输出行 j, trace, sample) 填体。每个输出行 = 两侧最近真实通道线性插值
|
||||||
|
// (a==b 时即原通道)。GPR 立方体稠密(每体素有值),无空洞 → 不置 kBlank。
|
||||||
// 沿测线按 stride 下采样:输出道 to → 源道 t = to*stride。
|
// 沿测线按 stride 下采样:输出道 to → 源道 t = to*stride。
|
||||||
geopro::core::BuiltI16 built;
|
geopro::core::BuiltI16 built;
|
||||||
built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz);
|
built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz);
|
||||||
for (int c = 0; c < channels; ++c) {
|
for (int j = 0; j < ny; ++j) {
|
||||||
const auto& chData = processed.volumeData[c];
|
const auto& chA = processed.volumeData[rows[j].a];
|
||||||
|
const auto& chB = processed.volumeData[rows[j].b];
|
||||||
|
const double wb = rows[j].wb, wa = 1.0 - wb;
|
||||||
for (int to = 0; to < nxOut; ++to) {
|
for (int to = 0; to < nxOut; ++to) {
|
||||||
const int t = to * stride;
|
const int t = to * stride;
|
||||||
const bool hasTrace = t < static_cast<int>(chData.size());
|
const bool hasA = t < static_cast<int>(chA.size());
|
||||||
|
const bool hasB = t < static_cast<int>(chB.size());
|
||||||
for (int s = 0; s < samples; ++s) {
|
for (int s = 0; s < samples; ++s) {
|
||||||
short v = 0;
|
const double va =
|
||||||
if (hasTrace && s < static_cast<int>(chData[t].size())) v = chData[t][s];
|
(hasA && s < static_cast<int>(chA[t].size())) ? chA[t][s] : 0.0;
|
||||||
// X=输出道 to、Y=通道 c、Z=样本 s。
|
const double vb =
|
||||||
built.vol.at(to, c, s) = quant.toQ(static_cast<double>(v));
|
(hasB && s < static_cast<int>(chB[t].size())) ? chB[t][s] : 0.0;
|
||||||
|
// X=输出道 to、Y=输出行 j、Z=样本 s。
|
||||||
|
built.vol.at(to, j, s) = quant.toQ(wa * va + wb * vb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +186,8 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
||||||
// 下采样后相邻输出道在世界中跨 stride 个原始道距 → dx ×stride 保持真实尺度。
|
// 下采样后相邻输出道在世界中跨 stride 个原始道距 → dx ×stride 保持真实尺度。
|
||||||
const double dxBase = h.distanceInc > 1e-9 ? h.distanceInc : 1.0;
|
const double dxBase = h.distanceInc > 1e-9 ? h.distanceInc : 1.0;
|
||||||
const double dx = dxBase * stride;
|
const double dx = dxBase * stride;
|
||||||
const double dy = channelSpacingY(h, channels);
|
// 插值后 Y 已规则化到 targetDy 网格;否则用原通道横距。
|
||||||
|
const double dy = interpolated ? targetDy : channelSpacingY(h, channels);
|
||||||
const double dz = depthSpacingZ(h);
|
const double dz = depthSpacingZ(h);
|
||||||
|
|
||||||
built.quant = quant;
|
built.quant = quant;
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,15 @@ struct BridgeMetrics {
|
||||||
// metricsOut 非空时回填维度/量化/spacing/耗时(供 CLI 报告,不编造)。
|
// metricsOut 非空时回填维度/量化/spacing/耗时(供 CLI 报告,不编造)。
|
||||||
// coarse(下采样因子,≥1):沿测线(道/X 轴)每 coarse 道取 1,spacing.x ×coarse 保形;
|
// coarse(下采样因子,≥1):沿测线(道/X 轴)每 coarse 道取 1,spacing.x ×coarse 保形;
|
||||||
// 通道/样本(横向/深度)保留全分辨率。coarse≤1 即全分辨率。磁盘紧张时省空间用。
|
// 通道/样本(横向/深度)保留全分辨率。coarse≤1 即全分辨率。磁盘紧张时省空间用。
|
||||||
|
// targetDy(米,>0 启用):线内【通道间插值】目标横向间距。读各通道真实横向偏移
|
||||||
|
// (header.chXOffsets) 规则网格化 Y 到 targetDy:ny=round(跨度/targetDy)+1,逐行线性
|
||||||
|
// 插值(不跨线、不假设道间距)。<=0 或无偏移 → 不插值,Y=原通道数。默认 0.025(2.5cm)。
|
||||||
// 失败(加载失败/立方体为空) → 抛 std::runtime_error。
|
// 失败(加载失败/立方体为空) → 抛 std::runtime_error。
|
||||||
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
|
||||||
const std::string& linePrefix,
|
const std::string& linePrefix,
|
||||||
BridgeMetrics* metricsOut,
|
BridgeMetrics* metricsOut,
|
||||||
int coarse = 1);
|
int coarse = 1,
|
||||||
|
double targetDy = 0.025);
|
||||||
|
|
||||||
} // namespace geopro::io::gpr
|
} // namespace geopro::io::gpr
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
#include "io/gpr/GprGeometry.hpp"
|
#include "io/gpr/GprGeometry.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <numeric>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -22,6 +25,46 @@ std::vector<double> parseChannelXOffsets(const std::string& ordText) {
|
||||||
return offsets;
|
return offsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ChannelInterpRow> planChannelInterpolation(
|
||||||
|
const std::vector<double>& offsets, double targetDy) {
|
||||||
|
const int n = static_cast<int>(offsets.size());
|
||||||
|
std::vector<ChannelInterpRow> rows;
|
||||||
|
// 退化:通道<2 或 targetDy 非法 → 逐通道 identity。
|
||||||
|
if (n < 2 || targetDy <= 0.0) {
|
||||||
|
for (int i = 0; i < n; ++i) rows.push_back({i, i, 0.0});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
// 按偏移排序的通道索引(端点 / 区间定位用;通道本身可能非有序)。
|
||||||
|
std::vector<int> ord(n);
|
||||||
|
std::iota(ord.begin(), ord.end(), 0);
|
||||||
|
std::sort(ord.begin(), ord.end(),
|
||||||
|
[&](int x, int y) { return offsets[x] < offsets[y]; });
|
||||||
|
const double mn = offsets[ord.front()];
|
||||||
|
const double mx = offsets[ord.back()];
|
||||||
|
const double span = mx - mn;
|
||||||
|
// 跨度已比 targetDy 还密 → 不加密,逐通道 identity(保原通道序)。
|
||||||
|
if (span <= targetDy * 0.5) {
|
||||||
|
for (int i = 0; i < n; ++i) rows.push_back({i, i, 0.0});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
const int ny = static_cast<int>(std::lround(span / targetDy)) + 1;
|
||||||
|
int k = 0; // ord 内区间左指针:offsets[ord[k]] <= p
|
||||||
|
for (int j = 0; j < ny; ++j) {
|
||||||
|
const double p = mn + static_cast<double>(j) * targetDy;
|
||||||
|
while (k + 1 < n && offsets[ord[k + 1]] < p) ++k;
|
||||||
|
if (k + 1 >= n) { // p 在最右通道之外 → 取最右通道
|
||||||
|
rows.push_back({ord[n - 1], ord[n - 1], 0.0});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int a = ord[k], b = ord[k + 1];
|
||||||
|
const double oa = offsets[a], ob = offsets[b];
|
||||||
|
double wb = (ob > oa) ? (p - oa) / (ob - oa) : 0.0;
|
||||||
|
wb = std::clamp(wb, 0.0, 1.0);
|
||||||
|
rows.push_back({a, b, wb});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
double depthOfSample(int s, const IprHeader& h) {
|
double depthOfSample(int s, const IprHeader& h) {
|
||||||
if (h.samples <= 1) return 0.0; // 防除零
|
if (h.samples <= 1) return 0.0; // 防除零
|
||||||
const double timeNs = static_cast<double>(s) * h.timeWindowNs /
|
const double timeNs = static_cast<double>(s) * h.timeWindowNs /
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,22 @@ namespace geopro::io::gpr {
|
||||||
// 解析 .ord 文本,返回末列==1 的有效通道的横向偏移(第 2 列),按文件顺序。
|
// 解析 .ord 文本,返回末列==1 的有效通道的横向偏移(第 2 列),按文件顺序。
|
||||||
std::vector<double> parseChannelXOffsets(const std::string& ordText);
|
std::vector<double> parseChannelXOffsets(const std::string& ordText);
|
||||||
|
|
||||||
|
// 通道间插值方案:一个输出网格行 = (1-wb)*通道[a] + wb*通道[b](线性)。
|
||||||
|
// a==b 时即原样取该通道(无插值)。
|
||||||
|
struct ChannelInterpRow {
|
||||||
|
int a = 0;
|
||||||
|
int b = 0;
|
||||||
|
double wb = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按真实横向偏移 offsets(米,逐通道) + 目标横向间距 targetDy(米) 规则网格化通道维(Y):
|
||||||
|
// 返回每个输出网格行的线性插值方案。网格在 [min(off), max(off)] 上以 targetDy 等距取
|
||||||
|
// ny = round(span/targetDy)+1 行;每行找两侧最近真实通道线性插值(端点外用端点)。
|
||||||
|
// 退化(通道<2 / targetDy<=0 / 跨度已比 targetDy 还密) → 逐通道 identity(每通道一行)。
|
||||||
|
// 纯函数,便于单测:不依赖任何文件/模型。
|
||||||
|
std::vector<ChannelInterpRow> planChannelInterpolation(
|
||||||
|
const std::vector<double>& offsets, double targetDy);
|
||||||
|
|
||||||
// 采样序号 s → 深度(米)。depth = soilVelocity[m/s] * (s * timeWindowNs/(samples-1) * 1e-9) / 2。
|
// 采样序号 s → 深度(米)。depth = soilVelocity[m/s] * (s * timeWindowNs/(samples-1) * 1e-9) / 2。
|
||||||
// samples<=1 时返回 0 防除零。
|
// samples<=1 时返回 0 防除零。
|
||||||
double depthOfSample(int s, const IprHeader& h);
|
double depthOfSample(int s, const IprHeader& h);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ namespace {
|
||||||
// 三维斜视方位角 / 仰角。
|
// 三维斜视方位角 / 仰角。
|
||||||
constexpr double kAzimuth = 30.0;
|
constexpr double kAzimuth = 30.0;
|
||||||
constexpr double kElevation = 25.0;
|
constexpr double kElevation = 25.0;
|
||||||
|
// 二维分析近俯视:自正俯视下压的角度(12°→俯角约78°)。留一点倾斜使高程差可辨。
|
||||||
|
constexpr double kNearTopTilt = 12.0;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void applyTop2D(vtkRenderer* r)
|
void applyTop2D(vtkRenderer* r)
|
||||||
|
|
@ -37,6 +39,20 @@ void applyFree3D(vtkRenderer* r)
|
||||||
r->ResetCamera();
|
r->ResetCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void applyNearTop2D(vtkRenderer* r)
|
||||||
|
{
|
||||||
|
if (!r) return;
|
||||||
|
auto* c = r->GetActiveCamera();
|
||||||
|
c->ParallelProjectionOff(); // 透视:配合一点倾斜,使高程差可见(正交/正俯视下不可辨)
|
||||||
|
// 自正俯视(+Z 向下看、北朝上)起,下压 kNearTopTilt → 俯角约 78°;方位不偏(正北俯视)。
|
||||||
|
c->SetFocalPoint(0, 0, 0);
|
||||||
|
c->SetPosition(0, 0, 1);
|
||||||
|
c->SetViewUp(0, 1, 0);
|
||||||
|
c->Elevation(kNearTopTilt);
|
||||||
|
c->OrthogonalizeViewUp();
|
||||||
|
r->ResetCamera();
|
||||||
|
}
|
||||||
|
|
||||||
void applyView(vtkRenderer* r, ViewDir dir)
|
void applyView(vtkRenderer* r, ViewDir dir)
|
||||||
{
|
{
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ void applyTop2D(vtkRenderer* r);
|
||||||
// 自由三维:透视投影,斜视方位看到剖面立体。
|
// 自由三维:透视投影,斜视方位看到剖面立体。
|
||||||
void applyFree3D(vtkRenderer* r);
|
void applyFree3D(vtkRenderer* r);
|
||||||
|
|
||||||
|
// 二维分析近俯视:透视投影,自正俯视下压一点(约12°→约78°俯角)。留一点倾斜使高程差可见
|
||||||
|
// (绝对正俯视下高程不可辨),仅平移+缩放(旋转由 interactor style 锁定)。
|
||||||
|
void applyNearTop2D(vtkRenderer* r);
|
||||||
|
|
||||||
// 快捷视图方向(世界系 x=East,y=North,z=-depth)。
|
// 快捷视图方向(世界系 x=East,y=North,z=-depth)。
|
||||||
// Top 俯视 (相机在 +Z 向下看)
|
// Top 俯视 (相机在 +Z 向下看)
|
||||||
// Bottom 仰视 (相机在 -Z 向上看)
|
// Bottom 仰视 (相机在 -Z 向上看)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ namespace {
|
||||||
// 虚线点画图案(16 位)与重复因子;dashed 异常用。
|
// 虚线点画图案(16 位)与重复因子;dashed 异常用。
|
||||||
constexpr int kDashPattern = 0xF0F0;
|
constexpr int kDashPattern = 0xF0F0;
|
||||||
constexpr int kDashRepeat = 1;
|
constexpr int kDashRepeat = 1;
|
||||||
// Point 型异常的方块点像素边长。
|
// Point 型异常的小球像素直径(RenderPointsAsSpheres 下为球径)。
|
||||||
constexpr float kPointSize = 8.0F;
|
constexpr float kPointSize = 13.0F;
|
||||||
|
|
||||||
// 把一个异常的 localPts 灌入 points(x, -y, 0:深度取负,与 #18 同坐标系)。
|
// 把一个异常的 localPts 灌入 points(x, -y, 0:深度取负,与 #18 同坐标系)。
|
||||||
void fillPoints2D(vtkPoints* points, const geopro::core::Anomaly& a)
|
void fillPoints2D(vtkPoints* points, const geopro::core::Anomaly& a)
|
||||||
|
|
@ -82,6 +82,7 @@ vtkSmartPointer<vtkActor> buildActor(vtkPoints* points, std::size_t n,
|
||||||
actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||||
if (asPoints) {
|
if (asPoints) {
|
||||||
actor->GetProperty()->SetPointSize(kPointSize);
|
actor->GetProperty()->SetPointSize(kPointSize);
|
||||||
|
actor->GetProperty()->SetRenderPointsAsSpheres(true); // 点异常渲染为小球(非扁平方点)
|
||||||
} else {
|
} else {
|
||||||
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
|
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
|
||||||
if (a.dashed) {
|
if (a.dashed) {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@
|
||||||
#include <vtkProperty.h>
|
#include <vtkProperty.h>
|
||||||
#include <vtkRenderWindowInteractor.h>
|
#include <vtkRenderWindowInteractor.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
#include <vtkTextActor.h>
|
|
||||||
#include <vtkTextProperty.h>
|
|
||||||
|
|
||||||
namespace geopro::render::interact {
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
|
@ -27,6 +25,7 @@ constexpr double kEps = 1e-9;
|
||||||
constexpr double kObserverPriority = 5.0; // 高于切片 widget 与右键菜单(1.0),绘制期独占
|
constexpr double kObserverPriority = 5.0; // 高于切片 widget 与右键菜单(1.0),绘制期独占
|
||||||
constexpr double kDoubleClickMs = 350.0; // 左键双击闭合阈值
|
constexpr double kDoubleClickMs = 350.0; // 左键双击闭合阈值
|
||||||
constexpr int kClickSlopPx = 6; // 双击位置相近阈值(px)
|
constexpr int kClickSlopPx = 6; // 双击位置相近阈值(px)
|
||||||
|
constexpr int kCloseSnapPx = 12; // 面:光标/点击邻近起点的吸附阈值(px)
|
||||||
|
|
||||||
double nowMs() {
|
double nowMs() {
|
||||||
return std::chrono::duration<double, std::milli>(
|
return std::chrono::duration<double, std::milli>(
|
||||||
|
|
@ -40,10 +39,11 @@ AnomalyDrawTool::AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRende
|
||||||
|
|
||||||
AnomalyDrawTool::~AnomalyDrawTool() { removeObservers(); }
|
AnomalyDrawTool::~AnomalyDrawTool() { removeObservers(); }
|
||||||
|
|
||||||
void AnomalyDrawTool::start(const Vec3& planeOrigin, const Vec3& planeNormal,
|
void AnomalyDrawTool::start(DrawMode mode, const Vec3& planeOrigin, const Vec3& planeNormal,
|
||||||
std::function<void(const std::vector<Vec3>&)> onFinish,
|
std::function<void(const std::vector<Vec3>&)> onFinish,
|
||||||
std::function<void()> onCancel) {
|
std::function<void()> onCancel) {
|
||||||
if (active_) cancel();
|
if (active_) cancel();
|
||||||
|
mode_ = mode;
|
||||||
origin_ = planeOrigin;
|
origin_ = planeOrigin;
|
||||||
normal_ = normalize(planeNormal);
|
normal_ = normalize(planeNormal);
|
||||||
onFinish_ = std::move(onFinish);
|
onFinish_ = std::move(onFinish);
|
||||||
|
|
@ -53,18 +53,8 @@ void AnomalyDrawTool::start(const Vec3& planeOrigin, const Vec3& planeNormal,
|
||||||
hasCursor_ = false;
|
hasCursor_ = false;
|
||||||
active_ = true;
|
active_ = true;
|
||||||
installObservers();
|
installObservers();
|
||||||
|
// 操作提示由 app 层 QLabel 浮层承担(VTK 内置字体不含中文字形 → vtkTextActor 渲染不出中文)。
|
||||||
// 屏幕操作提示(左上角),解决"不知如何闭合"。
|
|
||||||
if (renderer_) {
|
|
||||||
hint_ = vtkSmartPointer<vtkTextActor>::New();
|
|
||||||
hint_->SetInput("圈定异常:左键逐点 · 双击或右键完成 · Esc 取消");
|
|
||||||
hint_->GetTextProperty()->SetFontSize(16);
|
|
||||||
hint_->GetTextProperty()->SetColor(1.0, 0.9, 0.0);
|
|
||||||
hint_->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport();
|
|
||||||
hint_->GetPositionCoordinate()->SetValue(0.02, 0.94);
|
|
||||||
renderer_->AddViewProp(hint_);
|
|
||||||
if (interactor_) interactor_->Render();
|
if (interactor_) interactor_->Render();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnomalyDrawTool::cancel() {
|
void AnomalyDrawTool::cancel() {
|
||||||
|
|
@ -80,11 +70,9 @@ void AnomalyDrawTool::teardownActive() {
|
||||||
if (renderer_) {
|
if (renderer_) {
|
||||||
if (preview_) renderer_->RemoveViewProp(preview_);
|
if (preview_) renderer_->RemoveViewProp(preview_);
|
||||||
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
||||||
if (hint_) renderer_->RemoveViewProp(hint_);
|
|
||||||
}
|
}
|
||||||
preview_ = nullptr;
|
preview_ = nullptr;
|
||||||
rubber_ = nullptr;
|
rubber_ = nullptr;
|
||||||
hint_ = nullptr;
|
|
||||||
active_ = false;
|
active_ = false;
|
||||||
hasCursor_ = false;
|
hasCursor_ = false;
|
||||||
pts_.clear();
|
pts_.clear();
|
||||||
|
|
@ -115,7 +103,20 @@ Vec3 AnomalyDrawTool::pickOnPlane() const {
|
||||||
return Vec3{{nearP[0] + t * dir[0], nearP[1] + t * dir[1], nearP[2] + t * dir[2]}};
|
return Vec3{{nearP[0] + t * dir[0], nearP[1] + t * dir[1], nearP[2] + t * dir[2]}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AnomalyDrawTool::nearFirstVertex(int sx, int sy) const {
|
||||||
|
if (pts_.empty() || !renderer_) return false;
|
||||||
|
renderer_->SetWorldPoint(pts_.front()[0], pts_.front()[1], pts_.front()[2], 1.0);
|
||||||
|
renderer_->WorldToDisplay();
|
||||||
|
double d[3];
|
||||||
|
renderer_->GetDisplayPoint(d);
|
||||||
|
return std::abs(d[0] - sx) <= kCloseSnapPx && std::abs(d[1] - sy) <= kCloseSnapPx;
|
||||||
|
}
|
||||||
|
|
||||||
void AnomalyDrawTool::addVertex() {
|
void AnomalyDrawTool::addVertex() {
|
||||||
|
// 点模式:单点,再次左键 = 重定位(微调),不累积;线/面模式:累积顶点。
|
||||||
|
if (mode_ == DrawMode::Point && !pts_.empty())
|
||||||
|
pts_[0] = pickOnPlane();
|
||||||
|
else
|
||||||
pts_.push_back(pickOnPlane());
|
pts_.push_back(pickOnPlane());
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +159,8 @@ void AnomalyDrawTool::updatePreview() {
|
||||||
preview_->SetMapper(mapper);
|
preview_->SetMapper(mapper);
|
||||||
preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄
|
preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄
|
||||||
preview_->GetProperty()->SetLineWidth(2.0);
|
preview_->GetProperty()->SetLineWidth(2.0);
|
||||||
preview_->GetProperty()->SetPointSize(9.0); // 醒目圆点
|
preview_->GetProperty()->SetPointSize(mode_ == DrawMode::Point ? 16.0 : 9.0); // 点模式更醒目
|
||||||
|
preview_->GetProperty()->SetRenderPointsAsSpheres(true); // 顶点渲染为小球(图钉感)
|
||||||
renderer_->AddActor(preview_);
|
renderer_->AddActor(preview_);
|
||||||
interactor_->Render();
|
interactor_->Render();
|
||||||
}
|
}
|
||||||
|
|
@ -167,16 +169,18 @@ void AnomalyDrawTool::updateRubber() {
|
||||||
if (!renderer_) return;
|
if (!renderer_) return;
|
||||||
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
||||||
rubber_ = nullptr;
|
rubber_ = nullptr;
|
||||||
if (pts_.empty() || !hasCursor_) {
|
// 点模式:单点标注,不拉末点→光标的橡皮筋线(否则点完还甩出一条线,用户反馈)。
|
||||||
|
if (mode_ == DrawMode::Point || pts_.empty() || !hasCursor_) {
|
||||||
if (interactor_) interactor_->Render();
|
if (interactor_) interactor_->Render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 末点 → 当前光标投影点 的虚线橡皮筋(跟手反馈)。
|
// 末点 → 光标 的虚线橡皮筋(跟手反馈);面模式光标邻近起点 → 指向起点,预览闭合。
|
||||||
const Vec3& a = pts_.back();
|
const Vec3& a = pts_.back();
|
||||||
|
const Vec3 endP = cursorNearStart_ ? pts_.front() : cursorPt_;
|
||||||
vtkNew<vtkPoints> points;
|
vtkNew<vtkPoints> points;
|
||||||
points->SetNumberOfPoints(2);
|
points->SetNumberOfPoints(2);
|
||||||
points->SetPoint(0, a[0], a[1], a[2]);
|
points->SetPoint(0, a[0], a[1], a[2]);
|
||||||
points->SetPoint(1, cursorPt_[0], cursorPt_[1], cursorPt_[2]);
|
points->SetPoint(1, endP[0], endP[1], endP[2]);
|
||||||
vtkNew<vtkPolyData> poly;
|
vtkNew<vtkPolyData> poly;
|
||||||
poly->SetPoints(points);
|
poly->SetPoints(points);
|
||||||
vtkNew<vtkPolyLine> line;
|
vtkNew<vtkPolyLine> line;
|
||||||
|
|
@ -200,7 +204,9 @@ void AnomalyDrawTool::updateRubber() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnomalyDrawTool::finish() {
|
void AnomalyDrawTool::finish() {
|
||||||
if (pts_.size() < 3) { // 不足以成面 → 取消
|
const std::size_t minPts =
|
||||||
|
mode_ == DrawMode::Point ? 1 : (mode_ == DrawMode::Line ? 2 : 3); // 点1/线2/面3
|
||||||
|
if (pts_.size() < minPts) { // 不足以成形 → 取消
|
||||||
cancel();
|
cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +227,9 @@ void AnomalyDrawTool::installObservers() {
|
||||||
// 鼠标移动:更新末点→光标的虚线橡皮筋(跟手反馈)。不 abort,不干扰其它悬停。
|
// 鼠标移动:更新末点→光标的虚线橡皮筋(跟手反馈)。不 abort,不干扰其它悬停。
|
||||||
self->cursorPt_ = self->pickOnPlane();
|
self->cursorPt_ = self->pickOnPlane();
|
||||||
self->hasCursor_ = true;
|
self->hasCursor_ = true;
|
||||||
|
const int* mp = self->interactor_->GetEventPosition();
|
||||||
|
self->cursorNearStart_ = self->mode_ == DrawMode::Face && self->pts_.size() >= 3 &&
|
||||||
|
self->nearFirstVertex(mp[0], mp[1]);
|
||||||
self->updateRubber();
|
self->updateRubber();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -229,9 +238,21 @@ void AnomalyDrawTool::installObservers() {
|
||||||
if (self->cmd_) self->cmd_->SetAbortFlag(1);
|
if (self->cmd_) self->cmd_->SetAbortFlag(1);
|
||||||
switch (eid) {
|
switch (eid) {
|
||||||
case vtkCommand::LeftButtonPressEvent: {
|
case vtkCommand::LeftButtonPressEvent: {
|
||||||
// 左键双连击 = 闭合(标准多边形交互);否则加顶点。
|
// 点:单击即落点并完成(业界通用,无需双击/回车)。
|
||||||
|
if (self->mode_ == DrawMode::Point) {
|
||||||
|
self->addVertex();
|
||||||
|
self->finish();
|
||||||
|
break;
|
||||||
|
}
|
||||||
const double now = nowMs();
|
const double now = nowMs();
|
||||||
const int* p = self->interactor_->GetEventPosition();
|
const int* p = self->interactor_->GetEventPosition();
|
||||||
|
// 面:点回起点(屏幕邻近)闭合(≥3点),不加点。
|
||||||
|
if (self->mode_ == DrawMode::Face && self->pts_.size() >= 3 &&
|
||||||
|
self->nearFirstVertex(p[0], p[1])) {
|
||||||
|
self->finish();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 线:左键双连击 = 完成;否则加顶点。
|
||||||
const bool dbl = self->lastClickMs_ >= 0.0 &&
|
const bool dbl = self->lastClickMs_ >= 0.0 &&
|
||||||
(now - self->lastClickMs_) < kDoubleClickMs &&
|
(now - self->lastClickMs_) < kDoubleClickMs &&
|
||||||
std::abs(p[0] - self->lastClickX_) <= kClickSlopPx &&
|
std::abs(p[0] - self->lastClickX_) <= kClickSlopPx &&
|
||||||
|
|
@ -239,17 +260,29 @@ void AnomalyDrawTool::installObservers() {
|
||||||
self->lastClickMs_ = now;
|
self->lastClickMs_ = now;
|
||||||
self->lastClickX_ = p[0];
|
self->lastClickX_ = p[0];
|
||||||
self->lastClickY_ = p[1];
|
self->lastClickY_ = p[1];
|
||||||
if (dbl)
|
if (dbl) {
|
||||||
|
// 双击结束:第一下已落点(=双击位置),保留为末顶点直接完成(含双击位置,同地图工具)。
|
||||||
self->finish();
|
self->finish();
|
||||||
else
|
} else {
|
||||||
self->addVertex();
|
self->addVertex();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case vtkCommand::RightButtonPressEvent: self->finish(); break;
|
case vtkCommand::RightButtonPressEvent:
|
||||||
|
// 绘制中右键不提交(保留给「创建异常」菜单语义);已 abort 消费,不打开菜单。
|
||||||
|
break;
|
||||||
case vtkCommand::KeyPressEvent: {
|
case vtkCommand::KeyPressEvent: {
|
||||||
const char* key = self->interactor_->GetKeySym();
|
const char* key = self->interactor_->GetKeySym();
|
||||||
if (key && (std::string(key) == "Escape")) self->cancel();
|
const std::string k = key ? std::string(key) : std::string();
|
||||||
else if (key && (std::string(key) == "Return")) self->finish();
|
if (k == "Escape")
|
||||||
|
self->cancel();
|
||||||
|
else if (k == "Return" || k == "KP_Enter")
|
||||||
|
self->finish();
|
||||||
|
else if ((k == "BackSpace" || k == "Delete") && !self->pts_.empty()) {
|
||||||
|
self->pts_.pop_back(); // 撤上一点
|
||||||
|
self->updatePreview();
|
||||||
|
self->updateRubber();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: break;
|
default: break;
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,28 @@
|
||||||
class vtkRenderWindowInteractor;
|
class vtkRenderWindowInteractor;
|
||||||
class vtkRenderer;
|
class vtkRenderer;
|
||||||
class vtkActor;
|
class vtkActor;
|
||||||
class vtkTextActor;
|
|
||||||
class vtkCallbackCommand;
|
class vtkCallbackCommand;
|
||||||
|
|
||||||
namespace geopro::render::interact {
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
// 异常圈定工具(#4b):在给定切片平面上交互式画多边形。
|
// 异常圈定工具(#4b):在给定切片平面上交互式画 点 / 线 / 面。
|
||||||
// 左键逐点加顶点(屏幕射线与平面求交,落在平面上);右键 / 双击 / 回车 闭合 → onFinish(worldPts);
|
// 左键逐点加顶点(屏幕射线与平面求交,落在平面上);**双击 / 回车 提交** → onFinish(worldPts);
|
||||||
// Esc / 不足 3 点闭合 → onCancel。绘制中实时预览折线。
|
// Esc 取消;Backspace 撤上一点;点模式再次左键=重定位单点(微调)。右键绘制中不响应(保留给菜单语义)。
|
||||||
// 高优先级(2.0)交互器观察者抢输入:先于切片 widget 与 InteractionManager 右键菜单,绘制期独占左右键。
|
// 点≥1 / 线≥2(开放) / 面≥3(闭合);闭合与否由上层据 markType 渲染,本工具只产顶点。
|
||||||
|
// 高优先级(5.0)交互器观察者抢输入:先于切片 widget 与 InteractionManager 右键菜单,绘制期独占左右键。
|
||||||
// render 层:只碰 VTK,不认业务;产物(平面上的世界点)经回调交上层组装 core::Anomaly。
|
// render 层:只碰 VTK,不认业务;产物(平面上的世界点)经回调交上层组装 core::Anomaly。
|
||||||
class AnomalyDrawTool {
|
class AnomalyDrawTool {
|
||||||
public:
|
public:
|
||||||
|
enum class DrawMode { Point, Line, Face }; // 点(1)/线(2,开放)/面(3,闭合)
|
||||||
|
|
||||||
AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRenderer* renderer);
|
AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRenderer* renderer);
|
||||||
~AnomalyDrawTool();
|
~AnomalyDrawTool();
|
||||||
|
|
||||||
AnomalyDrawTool(const AnomalyDrawTool&) = delete;
|
AnomalyDrawTool(const AnomalyDrawTool&) = delete;
|
||||||
AnomalyDrawTool& operator=(const AnomalyDrawTool&) = delete;
|
AnomalyDrawTool& operator=(const AnomalyDrawTool&) = delete;
|
||||||
|
|
||||||
// 开始在平面(origin/normal)上圈定。onFinish 收闭合多边形顶点(世界系);onCancel 取消。
|
// 开始在平面(origin/normal)上按 mode 圈定。onFinish 收顶点(世界系);onCancel 取消。
|
||||||
void start(const Vec3& planeOrigin, const Vec3& planeNormal,
|
void start(DrawMode mode, const Vec3& planeOrigin, const Vec3& planeNormal,
|
||||||
std::function<void(const std::vector<Vec3>&)> onFinish,
|
std::function<void(const std::vector<Vec3>&)> onFinish,
|
||||||
std::function<void()> onCancel);
|
std::function<void()> onCancel);
|
||||||
bool active() const { return active_; }
|
bool active() const { return active_; }
|
||||||
|
|
@ -38,8 +40,9 @@ private:
|
||||||
void addVertex(); // 左键:加顶点
|
void addVertex(); // 左键:加顶点
|
||||||
void updatePreview(); // 重建已点几何(顶点圆点 + 实线折线;单点也可见)
|
void updatePreview(); // 重建已点几何(顶点圆点 + 实线折线;单点也可见)
|
||||||
void updateRubber(); // 鼠标移动:末点→光标的虚线橡皮筋
|
void updateRubber(); // 鼠标移动:末点→光标的虚线橡皮筋
|
||||||
void finish(); // 右键/双击/回车:闭合
|
void finish(); // 双击/回车/(面)点起点:完成
|
||||||
Vec3 pickOnPlane() const; // 当前鼠标屏幕点 → 射线与平面交点
|
Vec3 pickOnPlane() const; // 当前鼠标屏幕点 → 射线与平面交点
|
||||||
|
bool nearFirstVertex(int sx, int sy) const; // 屏幕点是否邻近起点(面闭合判定/提示)
|
||||||
|
|
||||||
void installObservers();
|
void installObservers();
|
||||||
void removeObservers();
|
void removeObservers();
|
||||||
|
|
@ -49,6 +52,7 @@ private:
|
||||||
vtkRenderer* renderer_;
|
vtkRenderer* renderer_;
|
||||||
|
|
||||||
bool active_ = false;
|
bool active_ = false;
|
||||||
|
DrawMode mode_ = DrawMode::Face;
|
||||||
Vec3 origin_{{0, 0, 0}}, normal_{{0, 0, 1}};
|
Vec3 origin_{{0, 0, 0}}, normal_{{0, 0, 1}};
|
||||||
std::vector<Vec3> pts_;
|
std::vector<Vec3> pts_;
|
||||||
std::function<void(const std::vector<Vec3>&)> onFinish_;
|
std::function<void(const std::vector<Vec3>&)> onFinish_;
|
||||||
|
|
@ -56,9 +60,9 @@ private:
|
||||||
|
|
||||||
vtkSmartPointer<vtkActor> preview_; // 已点几何(顶点圆点 + 实线折线)
|
vtkSmartPointer<vtkActor> preview_; // 已点几何(顶点圆点 + 实线折线)
|
||||||
vtkSmartPointer<vtkActor> rubber_; // 末点→光标 虚线橡皮筋
|
vtkSmartPointer<vtkActor> rubber_; // 末点→光标 虚线橡皮筋
|
||||||
vtkSmartPointer<vtkTextActor> hint_; // 屏幕操作提示
|
|
||||||
Vec3 cursorPt_{{0, 0, 0}}; // 当前鼠标在切面上的投影点
|
Vec3 cursorPt_{{0, 0, 0}}; // 当前鼠标在切面上的投影点
|
||||||
bool hasCursor_ = false;
|
bool hasCursor_ = false;
|
||||||
|
bool cursorNearStart_ = false; // 面模式光标邻近起点 → 橡皮筋指向起点预览闭合
|
||||||
vtkSmartPointer<vtkCallbackCommand> cmd_;
|
vtkSmartPointer<vtkCallbackCommand> cmd_;
|
||||||
unsigned long tagLeft_ = 0, tagMove_ = 0, tagRight_ = 0, tagKey_ = 0, tagDbl_ = 0;
|
unsigned long tagLeft_ = 0, tagMove_ = 0, tagRight_ = 0, tagKey_ = 0, tagDbl_ = 0;
|
||||||
// 双击闭合检测(左键两连击):记上次左键时刻 + 屏幕位置。
|
// 双击闭合检测(左键两连击):记上次左键时刻 + 屏幕位置。
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ void InteractionManager::showSavedSlice(const std::string& dsId, int axis, const
|
||||||
tool->setVolumeDsId(volumeDsId);
|
tool->setVolumeDsId(volumeDsId);
|
||||||
SliceTool* tp = tool.get();
|
SliceTool* tp = tool.get();
|
||||||
tool->onInteract = [this, tp]() { selectByTool(tp); };
|
tool->onInteract = [this, tp]() { selectByTool(tp); };
|
||||||
|
tool->setInteractive(false); // 已保存切片定稿锁定:不可移动/旋转(用户要求);仍可拾取选中/右键
|
||||||
slices_.push_back(std::move(tool));
|
slices_.push_back(std::move(tool));
|
||||||
selected_ = static_cast<int>(slices_.size()) - 1;
|
selected_ = static_cast<int>(slices_.size()) - 1;
|
||||||
updateSelectionVisual(); // 程序化显示(syncSlices):不发 onSliceSelectionChanged,避免列表选中被刷
|
updateSelectionVisual(); // 程序化显示(syncSlices):不发 onSliceSelectionChanged,避免列表选中被刷
|
||||||
|
|
@ -205,6 +206,13 @@ bool InteractionManager::selectSavedSlice(const std::string& dsId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InteractionManager::deselectSlice() {
|
||||||
|
if (selected_ < 0) return;
|
||||||
|
selected_ = -1;
|
||||||
|
updateSelectionVisual(); // 清高亮(无选中切片)
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
void InteractionManager::selectByTool(const SliceTool* tool) {
|
void InteractionManager::selectByTool(const SliceTool* tool) {
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
for (std::size_t i = 0; i < slices_.size(); ++i)
|
for (std::size_t i = 0; i < slices_.size(); ++i)
|
||||||
|
|
@ -252,6 +260,27 @@ 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();
|
||||||
|
|
@ -289,6 +318,7 @@ std::string InteractionManager::selectedSliceVolumeDsId() const {
|
||||||
void InteractionManager::tagSelectedSlice(const std::string& dsId) {
|
void InteractionManager::tagSelectedSlice(const std::string& dsId) {
|
||||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
|
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
|
||||||
slices_[static_cast<std::size_t>(selected_)]->setDsId(dsId);
|
slices_[static_cast<std::size_t>(selected_)]->setDsId(dsId);
|
||||||
|
slices_[static_cast<std::size_t>(selected_)]->setInteractive(false); // 保存即定稿锁定(不可改)
|
||||||
}
|
}
|
||||||
|
|
||||||
vtkImageData* InteractionManager::selectedSliceImage() const {
|
vtkImageData* InteractionManager::selectedSliceImage() const {
|
||||||
|
|
@ -297,37 +327,29 @@ vtkImageData* InteractionManager::selectedSliceImage() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() const {
|
vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() const {
|
||||||
vtkImageData* scalar = selectedSliceImage();
|
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
|
||||||
if (scalar == nullptr) return nullptr;
|
// 与屏幕切片**同源**的着色输出(widget 自己的 ColorMap 输出, 逐像素一致, RGBA 外区透明)。
|
||||||
|
// 原先另建 LUT 上色, 与屏幕配色可能不一致(用户实测异常截图与切面差异大) → 改取 widget 着色结果。
|
||||||
|
auto colored = slices_[static_cast<std::size_t>(selected_)]->coloredResliceImage();
|
||||||
|
if (colored == nullptr) return nullptr;
|
||||||
|
|
||||||
// 高清导出:切片重采样像素维度受体素网格分辨率限制(常仅几十px)→ 先上采样到目标分辨率
|
// 高清化:切片重采样像素维度受体素分辨率限制(常仅几十px) → 上采样到目标分辨率(双线性, 与屏幕
|
||||||
// (最长边 kExportLongSide,保持长宽比、插值),再上色,得到清晰大图。
|
// TextureInterpolateOn 同口径), 得清晰大图。对 RGBA 直接插值(色已定, 不再过 LUT)。
|
||||||
constexpr int kExportLongSide = 2048;
|
constexpr int kExportLongSide = 2048;
|
||||||
int dims[3];
|
int dims[3];
|
||||||
scalar->GetDimensions(dims);
|
colored->GetDimensions(dims);
|
||||||
const int nx = dims[0], ny = dims[1];
|
const int nx = dims[0], ny = dims[1];
|
||||||
const int longest = std::max(nx, ny);
|
const int longest = std::max(nx, ny);
|
||||||
double f = (longest > 0) ? static_cast<double>(kExportLongSide) / longest : 1.0;
|
double f = (longest > 0) ? static_cast<double>(kExportLongSide) / longest : 1.0;
|
||||||
if (f < 1.0) f = 1.0; // 不缩小(已够大则原样)
|
if (f < 1.0) f = 1.0; // 不缩小
|
||||||
vtkNew<vtkImageResize> resize;
|
vtkNew<vtkImageResize> resize;
|
||||||
resize->SetInputData(scalar);
|
resize->SetInputData(colored);
|
||||||
resize->SetResizeMethodToOutputDimensions();
|
resize->SetResizeMethodToOutputDimensions();
|
||||||
resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)),
|
resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)),
|
||||||
std::max(1, static_cast<int>(ny * f)), 1);
|
std::max(1, static_cast<int>(ny * f)), 1);
|
||||||
resize->Update();
|
resize->Update();
|
||||||
|
|
||||||
// 用与切片显示同一色阶 LUT 上色:取选中切片所属体的色阶(多体并发各体色阶不同)。
|
|
||||||
const VolumeImg* v = (selected_ >= 0 && selected_ < static_cast<int>(slices_.size()))
|
|
||||||
? volumeOf(slices_[static_cast<std::size_t>(selected_)]->volumeDsId())
|
|
||||||
: nullptr;
|
|
||||||
auto lut = v ? buildLut(v->cs, v->vmin, v->vmax) : buildLut(geopro::core::ColorScale{}, 0.0, 1.0);
|
|
||||||
vtkNew<vtkImageMapToColors> map;
|
|
||||||
map->SetInputConnection(resize->GetOutputPort());
|
|
||||||
map->SetLookupTable(lut);
|
|
||||||
map->SetOutputFormatToRGB();
|
|
||||||
map->Update();
|
|
||||||
auto out = vtkSmartPointer<vtkImageData>::New();
|
auto out = vtkSmartPointer<vtkImageData>::New();
|
||||||
out->DeepCopy(map->GetOutput()); // 深拷贝脱离 filter 生命周期
|
out->DeepCopy(resize->GetOutput()); // 脱离 filter 生命周期
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,17 @@ public:
|
||||||
std::vector<std::string> shownSavedSliceIds() const; // 当前已显示的已保存切片 dsId 列表
|
std::vector<std::string> shownSavedSliceIds() const; // 当前已显示的已保存切片 dsId 列表
|
||||||
// 选中已显示的某 dsId 切片(列表操作定位到对应渲染切片);找到返回 true。
|
// 选中已显示的某 dsId 切片(列表操作定位到对应渲染切片);找到返回 true。
|
||||||
bool selectSavedSlice(const std::string& dsId);
|
bool selectSavedSlice(const std::string& dsId);
|
||||||
|
// 清除切片选中(列表选中切到别的对象/异常时调用,否则 VTK 切片仍高亮,用户反馈)。
|
||||||
|
void deselectSlice();
|
||||||
|
|
||||||
// 关闭选中切片(E56)。无选中则忽略。
|
// 关闭选中切片(E56)。无选中则忽略。
|
||||||
void closeSelected();
|
void closeSelected();
|
||||||
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
|
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
|
||||||
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);
|
||||||
|
|
||||||
|
|
@ -106,6 +112,10 @@ 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,9 @@
|
||||||
#include "interact/PickInteractorStyle.hpp"
|
#include "interact/PickInteractorStyle.hpp"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include <vtkCallbackCommand.h>
|
||||||
#include <vtkCamera.h>
|
#include <vtkCamera.h>
|
||||||
#include <vtkCellPicker.h>
|
#include <vtkCellPicker.h>
|
||||||
#include <vtkMath.h>
|
#include <vtkMath.h>
|
||||||
|
|
@ -47,6 +49,22 @@ 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);
|
const bool hit = pickWorld(world);
|
||||||
|
|
||||||
|
|
@ -82,6 +100,7 @@ void PickInteractorStyle::OnLeftButtonDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void PickInteractorStyle::Rotate() {
|
void PickInteractorStyle::Rotate() {
|
||||||
|
if (lock2D_) return; // 二维分析禁旋转(仅平移+缩放)
|
||||||
Vec3 c;
|
Vec3 c;
|
||||||
if (!this->CurrentRenderer || !getRotateCenter || !getRotateCenter(c)) {
|
if (!this->CurrentRenderer || !getRotateCenter || !getRotateCenter(c)) {
|
||||||
Superclass::Rotate(); // 无选中物 → 默认绕焦点旋转
|
Superclass::Rotate(); // 无选中物 → 默认绕焦点旋转
|
||||||
|
|
@ -126,12 +145,62 @@ 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() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PickInteractorStyle::OnLeftButtonUp() {
|
||||||
|
if (dragging2D_) { // 结束 Z 拖动
|
||||||
|
dragging2D_ = false;
|
||||||
|
if (this->Interactor) this->ReleaseFocus();
|
||||||
|
if (onDrag2DEnd) onDrag2DEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,23 @@ public:
|
||||||
// 在"按下开始拖动"时调用一次,把焦点设到该中心(位置同步补偿,画面不变)→ 之后绕它旋转、不跳。
|
// 在"按下开始拖动"时调用一次,把焦点设到该中心(位置同步补偿,画面不变)→ 之后绕它旋转、不跳。
|
||||||
std::function<bool(Vec3& center)> getRotateCenter;
|
std::function<bool(Vec3& center)> getRotateCenter;
|
||||||
|
|
||||||
|
// 二维分析锁:开 → 左键拖动改为平移、禁旋转(仅平移+缩放);关 → 恢复三维拾取/旋转交互。
|
||||||
|
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 OnLeftButtonUp() override;
|
||||||
|
|
||||||
void OnLeftButtonDown() override;
|
void OnLeftButtonDown() override;
|
||||||
void OnMouseWheelForward() override;
|
void OnMouseWheelForward() override;
|
||||||
void OnMouseWheelBackward() override;
|
void OnMouseWheelBackward() override;
|
||||||
|
|
@ -44,11 +61,20 @@ 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 且位置相近视为双击。
|
||||||
double lastDownTime_ = -1.0; // 单调时钟(毫秒),-1=无
|
double lastDownTime_ = -1.0; // 单调时钟(毫秒),-1=无
|
||||||
int lastDownPos_[2] = {0, 0};
|
int lastDownPos_[2] = {0, 0};
|
||||||
|
|
||||||
|
// 二维分析模式:左键=平移、禁旋转(仅平移+缩放)。由 InteractionManager 在切 tab 时设。
|
||||||
|
bool lock2D_ = false;
|
||||||
|
|
||||||
|
// B 期足迹 Z 拖动状态:左键命中足迹时进入,记上次鼠标 y 以算增量。
|
||||||
|
bool dragging2D_ = false;
|
||||||
|
int lastDragY_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::render::interact
|
} // namespace geopro::render::interact
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <vtkCallbackCommand.h>
|
#include <vtkCallbackCommand.h>
|
||||||
#include <vtkCommand.h>
|
#include <vtkCommand.h>
|
||||||
#include <vtkImageData.h>
|
#include <vtkImageData.h>
|
||||||
|
#include <vtkImageMapToColors.h>
|
||||||
#include <vtkImagePlaneWidget.h>
|
#include <vtkImagePlaneWidget.h>
|
||||||
#include <vtkLookupTable.h>
|
#include <vtkLookupTable.h>
|
||||||
#include <vtkProperty.h>
|
#include <vtkProperty.h>
|
||||||
|
|
@ -170,6 +171,27 @@ vtkImageData* SliceTool::reslicedOutput() const {
|
||||||
return widget_ ? widget_->GetResliceOutput() : nullptr;
|
return widget_ ? widget_->GetResliceOutput() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SliceTool::setInteractive(bool on) {
|
||||||
|
interactive_ = on; // 记录锁定态:setVisible 重显时复原
|
||||||
|
if (widget_) widget_->SetInteraction(on ? 1 : 0); // 关=锁移动/旋转/光标,纹理仍显示
|
||||||
|
}
|
||||||
|
|
||||||
|
void SliceTool::setVisible(bool on) {
|
||||||
|
if (!widget_) return;
|
||||||
|
widget_->SetEnabled(on ? 1 : 0); // 翻显隐(不销毁):几何/纹理保留、切回零重建
|
||||||
|
if (on) widget_->SetInteraction(interactive_ ? 1 : 0); // SetEnabled 可能重置交互→复原锁定态
|
||||||
|
}
|
||||||
|
|
||||||
|
vtkSmartPointer<vtkImageData> SliceTool::coloredResliceImage() const {
|
||||||
|
if (!widget_) return nullptr;
|
||||||
|
vtkImageMapToColors* cm = widget_->GetColorMap(); // widget 内部把 reslice 经 LUT 上色 → 纹理
|
||||||
|
if (cm == nullptr) return nullptr;
|
||||||
|
cm->Update();
|
||||||
|
auto out = vtkSmartPointer<vtkImageData>::New();
|
||||||
|
out->DeepCopy(cm->GetOutput()); // 即屏幕切片所贴像素(RGBA, 外区 alpha=0)
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
double SliceTool::distanceToPlane(const Vec3& p) const {
|
double SliceTool::distanceToPlane(const Vec3& p) const {
|
||||||
const Vec3 c = center();
|
const Vec3 c = center();
|
||||||
const Vec3 n = normal();
|
const Vec3 n = normal();
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,23 @@ public:
|
||||||
|
|
||||||
// 当前切面重采样得到的 2D 标量影像(导出 dat 用);widget 已释放则 nullptr。
|
// 当前切面重采样得到的 2D 标量影像(导出 dat 用);widget 已释放则 nullptr。
|
||||||
vtkImageData* reslicedOutput() const;
|
vtkImageData* reslicedOutput() const;
|
||||||
|
// 与屏幕切片纹理同源的着色输出(widget 自己的 ColorMap 输出, RGBA, 逐像素一致, 外区透明)。
|
||||||
|
// 异常截图/导出用它而非另建 LUT,避免与屏幕配色不一致(用户实测差异大)。
|
||||||
|
vtkSmartPointer<vtkImageData> coloredResliceImage() const;
|
||||||
|
// 开/关 widget 鼠标交互(移动/旋转/光标)。关=锁定但仍显示(已保存切片定稿不可改);
|
||||||
|
// 拾取选中/右键菜单由 PickInteractorStyle 独立处理,不受此影响。
|
||||||
|
void setInteractive(bool on);
|
||||||
|
|
||||||
|
// 显/隐切片(切到二维分析时隐藏,切回再显):SetEnabled 翻显隐而非销毁,几何/位置保留、
|
||||||
|
// 切回零重建。重显时复原锁定态(SetEnabled 可能把交互重置为开)。
|
||||||
|
void setVisible(bool on);
|
||||||
|
|
||||||
// 关闭:Off() 并解除 interactor 绑定(幂等)。
|
// 关闭:Off() 并解除 interactor 绑定(幂等)。
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SliceAxis axis_;
|
SliceAxis axis_;
|
||||||
|
bool interactive_ = true; // 当前是否允许交互(setInteractive 记录):重显(setVisible)时复原锁定态
|
||||||
std::string dsId_; // 已保存切片归属标签(空=临时交互切片)
|
std::string dsId_; // 已保存切片归属标签(空=临时交互切片)
|
||||||
std::string volumeDsId_; // 所属三维体 dsId(多体并发用)
|
std::string volumeDsId_; // 所属三维体 dsId(多体并发用)
|
||||||
vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证
|
vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证
|
||||||
|
|
|
||||||
|
|
@ -489,6 +489,40 @@ TEST(VtkSceneController, TwoDFootprintCoexistsWith3DCurtain) {
|
||||||
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 重加)。
|
// 自定义摆放(4) → worldZ=customZ;改摆放重摆已勾选足迹(移除旧 + 按新 Z 重加)。
|
||||||
TEST(VtkSceneController, TwoDPlacementCustomZAndReplace) {
|
TEST(VtkSceneController, TwoDPlacementCustomZAndReplace) {
|
||||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,13 @@ TEST(LocalSample3dRepo, DimensionOfMapsDdCode) {
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_section")), DsDimension::Dim3D);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_section")), DsDimension::Dim3D);
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_inversion_data")), DsDimension::Dim3D);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_inversion_data")), DsDimension::Dim3D);
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_slice")), DsDimension::Analysis3D);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_slice")), DsDimension::Analysis3D);
|
||||||
|
// 足迹型 → 二维:数据字典 DD0623 只 dd_trajectory_data 为统一通用轨迹「保留」;
|
||||||
|
// 瞬变电磁/雷达通道/RTK 等轨迹型字典均「删除」→ 不再归 2D(落 Other)。
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_trajectory_data")), DsDimension::Dim2D);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_trajectory_data")), DsDimension::Dim2D);
|
||||||
// 足迹型(各类轨迹) → 二维(spec §4.1)。
|
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_transient_electromagnetic_trajectory_data")),
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_transient_electromagnetic_trajectory_data")),
|
||||||
DsDimension::Dim2D);
|
DsDimension::Other);
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_radar_channel_trajectory")), DsDimension::Dim2D);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_radar_channel_trajectory")), DsDimension::Other);
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_radar_rtk_trajectory")), DsDimension::Dim2D);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_radar_rtk_trajectory")), DsDimension::Other);
|
||||||
EXPECT_EQ(repo.dimensionOf(rowWith("dd_unknown_xyz")), DsDimension::Other);
|
EXPECT_EQ(repo.dimensionOf(rowWith("dd_unknown_xyz")), DsDimension::Other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ namespace {
|
||||||
void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces,
|
void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces,
|
||||||
std::int16_t base, double chYOffset,
|
std::int16_t base, double chYOffset,
|
||||||
double distanceInterval, double timeWindowNs,
|
double distanceInterval, double timeWindowNs,
|
||||||
double soilVelocity, int channels) {
|
double soilVelocity, int channels,
|
||||||
|
double chXOffset = 1e30) { // 1e30=不写 CH_X_OFFSET(不触发通道插值)
|
||||||
std::ofstream h(iprhPath);
|
std::ofstream h(iprhPath);
|
||||||
h << "SAMPLES: " << samples << "\n";
|
h << "SAMPLES: " << samples << "\n";
|
||||||
h << "LAST TRACE: " << (traces - 1) << "\n";
|
h << "LAST TRACE: " << (traces - 1) << "\n";
|
||||||
|
|
@ -31,6 +32,7 @@ void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces,
|
||||||
h << "SOIL VELOCITY: " << soilVelocity << "\n";
|
h << "SOIL VELOCITY: " << soilVelocity << "\n";
|
||||||
h << "DISTANCE INTERVAL: " << distanceInterval << "\n";
|
h << "DISTANCE INTERVAL: " << distanceInterval << "\n";
|
||||||
h << "CH_Y_OFFSET: " << chYOffset << "\n";
|
h << "CH_Y_OFFSET: " << chYOffset << "\n";
|
||||||
|
if (chXOffset < 1e29) h << "CH_X_OFFSET: " << chXOffset << "\n"; // 横向偏移(插值用)
|
||||||
h.close();
|
h.close();
|
||||||
|
|
||||||
fs::path iprbPath = iprhPath;
|
fs::path iprbPath = iprhPath;
|
||||||
|
|
@ -119,6 +121,39 @@ TEST_F(Gpr3dvBridgeTest, MapsAxesQuantAndSpacing) {
|
||||||
geopro::core::ScalarVolumeI16::kBlank);
|
geopro::core::ScalarVolumeI16::kBlank);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(Gpr3dvBridgeTest, ChannelInterpDensifiesYThroughBridge) {
|
||||||
|
// §1 端到端:3 通道带真实横向偏移 CH_X_OFFSET=-0.5/0/+0.5(跨度 1.0)。
|
||||||
|
// targetDy=0.25 → ny=round(1.0/0.25)+1=5(从 3 通道插值加密到 5 平面),dy=0.25。
|
||||||
|
const int samples = 64, traces = 40, channels = 3;
|
||||||
|
writeSyntheticChannel(dir_ / "syn_001_A01.iprh", samples, traces, 100, -1.5,
|
||||||
|
0.05, 100.0, 0.1, channels, /*chXOffset=*/-0.5);
|
||||||
|
writeSyntheticChannel(dir_ / "syn_001_A02.iprh", samples, traces, 200, -1.5,
|
||||||
|
0.05, 100.0, 0.1, channels, /*chXOffset=*/0.0);
|
||||||
|
writeSyntheticChannel(dir_ / "syn_001_A03.iprh", samples, traces, 300, -1.5,
|
||||||
|
0.05, 100.0, 0.1, channels, /*chXOffset=*/0.5);
|
||||||
|
|
||||||
|
geopro::io::gpr::BridgeMetrics bm;
|
||||||
|
geopro::core::BuiltI16 built;
|
||||||
|
ASSERT_NO_THROW({
|
||||||
|
built = geopro::io::gpr::buildLineVolumeFromGpr3dv(
|
||||||
|
dir_.string(), "syn_001", &bm, /*coarse=*/1, /*targetDy=*/0.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_EQ(built.vol.ny(), 5); // 3 通道 → 5 网格平面
|
||||||
|
EXPECT_EQ(built.vol.ny(), bm.ny);
|
||||||
|
EXPECT_NEAR(built.spacing[1], 0.25, 1e-9); // dy = targetDy
|
||||||
|
// 稠密(无 kBlank);插值值落在原通道值域内(线性混合不外溢)。
|
||||||
|
EXPECT_NE(built.vol.at(0, 2, 0), geopro::core::ScalarVolumeI16::kBlank);
|
||||||
|
|
||||||
|
// 关闭插值(targetDy=0)→ ny 回到原通道数 3。
|
||||||
|
geopro::core::BuiltI16 raw;
|
||||||
|
ASSERT_NO_THROW({
|
||||||
|
raw = geopro::io::gpr::buildLineVolumeFromGpr3dv(
|
||||||
|
dir_.string(), "syn_001", nullptr, /*coarse=*/1, /*targetDy=*/0.0);
|
||||||
|
});
|
||||||
|
EXPECT_EQ(raw.vol.ny(), channels);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(Gpr3dvBridgeTest, ThrowsOnMissingLine) {
|
TEST_F(Gpr3dvBridgeTest, ThrowsOnMissingLine) {
|
||||||
// 目录无任何 _A.iprh → loadImpulseMultiChannel 失败 → 抛异常。
|
// 目录无任何 _A.iprh → loadImpulseMultiChannel 失败 → 抛异常。
|
||||||
geopro::io::gpr::BridgeMetrics bm;
|
geopro::io::gpr::BridgeMetrics bm;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#include "io/gpr/GprGeometry.hpp"
|
#include "io/gpr/GprGeometry.hpp"
|
||||||
#include "io/gpr/IprHeader.hpp"
|
#include "io/gpr/IprHeader.hpp"
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
using namespace geopro::io::gpr;
|
using namespace geopro::io::gpr;
|
||||||
|
|
||||||
TEST(GprGeometry, ParsesActiveChannelOffsets) {
|
TEST(GprGeometry, ParsesActiveChannelOffsets) {
|
||||||
|
|
@ -11,6 +13,69 @@ TEST(GprGeometry, ParsesActiveChannelOffsets) {
|
||||||
EXPECT_NEAR(xs[1], -0.581, 1e-6);
|
EXPECT_NEAR(xs[1], -0.581, 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(GprGeometry, ChannelInterpDensifiesToTargetGrid) {
|
||||||
|
// 14 通道 ~均匀 0.105m,跨度 1.372m;targetDy=0.025 → ny=round(1.372/0.025)+1=56。
|
||||||
|
std::vector<double> off;
|
||||||
|
for (int i = 0; i < 14; ++i) off.push_back(-0.686 + i * 0.1055385); // -0.686..+0.686
|
||||||
|
auto rows = planChannelInterpolation(off, 0.025);
|
||||||
|
EXPECT_EQ(rows.size(), 56u);
|
||||||
|
// 首行=最左通道(无插值),末行=最右通道。
|
||||||
|
EXPECT_EQ(rows.front().a, 0);
|
||||||
|
EXPECT_NEAR(rows.front().wb, 0.0, 1e-9);
|
||||||
|
EXPECT_EQ(rows.back().a, 13);
|
||||||
|
// 中间存在真插值行(wb 落在 (0,1))。
|
||||||
|
bool sawInterp = false;
|
||||||
|
for (const auto& r : rows)
|
||||||
|
if (r.a != r.b && r.wb > 0.05 && r.wb < 0.95) sawInterp = true;
|
||||||
|
EXPECT_TRUE(sawInterp);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(GprGeometry, ChannelInterpDynamicCountBySpacing) {
|
||||||
|
// 不同道间距 → 不同插值条数(动态,不写死)。两通道 0.10m 间距:
|
||||||
|
// targetDy=0.025 → ny=round(0.10/0.025)+1=5(端点 + 中间 3 条)。
|
||||||
|
auto r1 = planChannelInterpolation({0.0, 0.10}, 0.025);
|
||||||
|
EXPECT_EQ(r1.size(), 5u);
|
||||||
|
// targetDy=0.05 → ny=round(0.10/0.05)+1=3(端点 + 中间 1 条)。
|
||||||
|
auto r2 = planChannelInterpolation({0.0, 0.10}, 0.05);
|
||||||
|
EXPECT_EQ(r2.size(), 3u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(GprGeometry, ChannelInterpHandlesUnsortedOffsets) {
|
||||||
|
// 通道在文件里可能非按偏移有序(任意排布)。偏移乱序但物理跨度相同 → 网格行数、
|
||||||
|
// 端点、单调性应不受顺序影响(内部按偏移排序定位)。
|
||||||
|
std::vector<double> sorted = {-0.4, -0.2, 0.0, 0.2, 0.4};
|
||||||
|
std::vector<double> shuffled = {0.0, 0.4, -0.4, 0.2, -0.2}; // 同偏移集合,乱序
|
||||||
|
auto rs = planChannelInterpolation(sorted, 0.1);
|
||||||
|
auto ru = planChannelInterpolation(shuffled, 0.1);
|
||||||
|
ASSERT_EQ(rs.size(), ru.size()); // ny 仅取决于跨度,与顺序无关
|
||||||
|
EXPECT_EQ(rs.size(), 9u); // round(0.8/0.1)+1
|
||||||
|
// 真正的不变量:每行的【有效插值位置】=(1-wb)*off[a]+wb*off[b] 应等于网格位置
|
||||||
|
// p=min+j*targetDy(与通道在文件里的顺序无关)。末行恰落在 max 时会以 [次末,末] 夹
|
||||||
|
// 且 wb=1(值=末通道),故不能直接断言 a==末通道,要看有效位置。
|
||||||
|
auto effPos = [](const std::vector<double>& off, const ChannelInterpRow& r) {
|
||||||
|
return (1.0 - r.wb) * off[r.a] + r.wb * off[r.b];
|
||||||
|
};
|
||||||
|
for (std::size_t j = 0; j < ru.size(); ++j) {
|
||||||
|
const double p = -0.4 + static_cast<double>(j) * 0.1;
|
||||||
|
EXPECT_NEAR(effPos(sorted, rs[j]), p, 1e-9); // 有序
|
||||||
|
EXPECT_NEAR(effPos(shuffled, ru[j]), p, 1e-9); // 乱序:同一结果
|
||||||
|
// a/b 偏移夹住网格位置。
|
||||||
|
const double oa = shuffled[ru[j].a], ob = shuffled[ru[j].b];
|
||||||
|
EXPECT_LE(std::min(oa, ob) - 1e-9, p);
|
||||||
|
EXPECT_GE(std::max(oa, ob) + 1e-9, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(GprGeometry, ChannelInterpDegenerateIdentity) {
|
||||||
|
// 单通道 / 已比 targetDy 密 / targetDy<=0 → 逐通道 identity。
|
||||||
|
EXPECT_EQ(planChannelInterpolation({0.5}, 0.025).size(), 1u);
|
||||||
|
auto dense = planChannelInterpolation({0.0, 0.01}, 0.025); // 跨度<targetDy/2
|
||||||
|
ASSERT_EQ(dense.size(), 2u);
|
||||||
|
EXPECT_EQ(dense[0].a, 0);
|
||||||
|
EXPECT_EQ(dense[1].a, 1);
|
||||||
|
EXPECT_EQ(planChannelInterpolation({0.0, 0.10}, -1.0).size(), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(GprGeometry, DepthOfLastSampleMatchesPhysics) {
|
TEST(GprGeometry, DepthOfLastSampleMatchesPhysics) {
|
||||||
IprHeader h{};
|
IprHeader h{};
|
||||||
h.samples = 821;
|
h.samples = 821;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue