Compare commits
7 Commits
a2e16e18e8
...
b261374cc9
| Author | SHA1 | Date |
|---|---|---|
|
|
b261374cc9 | |
|
|
4835528b99 | |
|
|
209d85536c | |
|
|
5d1384d2a9 | |
|
|
1ad0e372cc | |
|
|
4a680cc18a | |
|
|
4fb7bb1e52 |
|
|
@ -40,11 +40,11 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 客户端当前做法(供确认/纠正)
|
## 3. 客户端做法演变
|
||||||
|
|
||||||
- 竖直坐标用 **`y` 作深度**:每个格子 Z = `-y[j]`(深度向下取负),**未使用 `z` 和 `elevation`**。
|
- **早期(已废弃)**:竖直 Z = `-y[j]`(把 `y` 当"深度向下"),平顶、不随地表。加真实地形底图后暴露问题:剖面整体沉到地下。
|
||||||
- 这与**二维"数据详情"反演图一致**(详情图 `ContourPlotItem` 也只用 `x`、`y` 画"距离 × 深度"矩形,不用 `z`/`elevation`)。
|
- **当前(2026-06-17)**:竖直 Z = `+y[j]`(把 `y` 当**真实高程**),与同样按真实高程渲染的地形底图同系对齐 → 剖面顶≈地表、露出地面(复刻原版观感)。详见 §6。
|
||||||
- 即:3D 帘面 = 二维"距离×深度"剖面**立起来 + 沿真实测线(lat/lon)弯曲**,**平顶**、不随地表起伏。
|
- 仍**未使用 `z` 和 `alt/elevation`**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -66,3 +66,17 @@
|
||||||
|
|
||||||
- 现状 (A) 已能正确渲染(与 2D 详情一致),可继续使用。
|
- 现状 (A) 已能正确渲染(与 2D 详情一致),可继续使用。
|
||||||
- 若业务要 (B) 地形跟随,需上面第 2/3/4 项的明确定义后,客户端按真实竖向模型实现(避免凭猜测导致渲染错误)。
|
- 若业务要 (B) 地形跟随,需上面第 2/3/4 项的明确定义后,客户端按真实竖向模型实现(避免凭猜测导致渲染错误)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 进展(2026-06-17):原版 web 代码已部分解答,但仍有跨数据集风险
|
||||||
|
|
||||||
|
拿到原版 `commercial-admin` 代码(`dataView/threeMap/threeMap.vue` `addEntityToMap`)后确认其做法:
|
||||||
|
- **`data.y` = 真实高程**(不是深度)。原版 `originY = data.y[0]`、`localY = data.y - originY`(相对形状),剖面世界 Z = `originY + localY = data.y`,并用 **`alt`(测线每点地表高程)** 算坡度/地表偏移;地形用 Mapbox 真实 DEM。两者同在真实高程系 → 剖面顶≈地表、露出地面。
|
||||||
|
- **无垂直夸张**(`scale.y = 1`,真实比例)。
|
||||||
|
|
||||||
|
**客户端据此已改为 Z = `+y`(§3 当前),与原版完全一致**(实证:`threeMap.vue:676` `position.z = originY + zCorrection`,`originY = data.y[0]`,注释明说"剖面底部对应真实海拔 originY",网格 `localY = data.y - originY`,`rotateX` 后竖向 = `data.y`)。
|
||||||
|
|
||||||
|
**原版的 `alt` 仅用于**:水平定位(`geo2pos` 取 `.x/.y`)+ 坡度补偿 `zCorrection`(而对应的 `rotateZ` 在 667 行被注释掉、实际不生效)。**原版并不用 alt 把剖面顶贴地表**——之前文档此处的"alt 贴地表"为臆测,已更正。
|
||||||
|
|
||||||
|
**⚠️ §2 表中 `y` 跨数据集不一致是数据层面问题,原版同样存在**:原版也按 `data.y` 摆放,对 `y≠真实高程` 的数据集(香港 `T120526003-3` `y`=13~26 而 `elevation`=41~51)一样会整体偏离地表。客户端与原版行为一致,非客户端 bug。若要纠正,属数据/业务侧(统一 `y` 的高程基准),非渲染侧凭 alt 推算。
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,87 @@
|
||||||
# 交接:VTK 三维视图「补充需求」(feat/vtk-3d-view)
|
# 交接:VTK 三维视图(feat/vtk-3d-view)
|
||||||
|
|
||||||
> 给下一个会话无缝接手用。日期 2026-06-16。分支 `feat/vtk-3d-view`,HEAD `07f2f25`,工作树干净(仅根目录 grid-*.png/grid-snap.yml 是既有未跟踪文件,非本任务产物,勿动)。
|
> 给下一个会话无缝接手用。更新日期 2026-06-17。分支 `feat/vtk-3d-view`,工作树:仅根目录 `grid-list-original.png`/`grid-list-small.png`/`grid-snap.yml`/`orig-dataview.png` 及 `docs/superpowers/specs/2026-06-17-web-embed-subpage-mount-design.md` 是**既有未跟踪文件,非本任务产物,勿动/勿提交**(曾被 `git add -A` 误纳、已撤回)。
|
||||||
|
|
||||||
## 1. 背景
|
## 1. 背景
|
||||||
- 项目:geopro 桌面客户端(Qt6 + VTK9 + ADS dock),Windows/MSVC+Ninja。
|
- 项目:geopro 桌面客户端(Qt6 + VTK9 + Qt-ADS dock),Windows/MSVC+Ninja,`build.bat`。
|
||||||
- 任务:实现需求表「补充需求」页签 = **VTK 三维视图的整套交互/结构**。需求源:`D:\Projects\GEOPRO\Geopro3.0 需求表.xlsx`「补充需求」(用 openpyxl 读,控制台中文乱码须导 UTF-8 文件再读;A1–C92)。
|
- 任务:实现需求表「补充需求」页 = VTK 三维视图整套结构/交互。需求源:`D:\Projects\GEOPRO\Geopro3.0 需求表.xlsx`「补充需求」页。
|
||||||
- 历史:做本需求前已有"基于本地样本数据的原型渲染"(帘面/体素/切片/地形/散点),但在 commit `6241eb3`"CentralScene 数据驱动重构"时**装配代码被摘除**——render 层 actor 完整且有测试,只是没接上。本任务从复活它起步。
|
- 原版 web 源码在 **`D:\Git\lanbingtech\commercial-admin`**(Vue + three-tile),是**复刻的权威参照**(threeMap.vue / mapSource.js / src/apis/)。
|
||||||
- 原版 web「数据视图」(`tenant.geomative.cn/#/projectSpace/dataView`) 已 Playwright 实地分析:3D = **ThreeTile(Three.js)地球**(非 Cesium)+多瓦片源;3D 结果=2D 反演剖面成竖直帘面。**三栏/切片是客户端新需求(web 无三栏)**。详见记忆 [[web-3d-view-threetile]]。
|
- 三栏结构(三维数据集 / 二维数据集 / 三维分析)+ 真实 ERT 反演剖面(帘面)+ 底图地形——这些**已完成**。本会话主要做了**底图/地形 + 剖面垂直配准 + 增量渲染**,并为下一阶段(三维体/切片/异常)做了**设计定稿**。
|
||||||
|
|
||||||
## 2. 关键约束(用户拍板)
|
## 2. 本会话已完成(均已编译绿 + 提交;用户验收"差不多了")
|
||||||
- **后端未就绪** → 本轮全部用 `LocalSampleRepository` 静态样本数据驱动;**但仓储接口必须按真实后端形态设计好**(`I3dSceneRepository` 异步),将来换 `Api3dRepository` 不动上层。
|
**底图 + 真实地形**(核心在 `src/app/TileBasemap.{hpp,cpp}`):
|
||||||
- **严格按需求,禁止砍功能/改需求**(教训:曾把 F25 砍掉、把双击正视改按钮,被用户纠正)。复刻不确定处必须实地学习(Playwright),禁猜测。
|
- 影像=**天地图卫星**(`img_w`,tk 内嵌);地形=**Mapbox terrain-RGB DEM**(原版同源,pk token 内嵌;`elev=-10000+(R*65536+G*256+B)*0.1`)。
|
||||||
|
- **四叉树多级 LOD**(按瓦片屏幕像素误差递归细分,近细远粗)。根 `kRootZoom=9`、阈值 `kTargetPx=384`、叶上限 `kMaxLeaves=200`。
|
||||||
|
- **视锥剔除只用 4 个侧面**(不用近/远裁剪面——远裁剪面随已加载几何变化会误剔除远块)。`frustum_[24]` 用焦点自校正法向。
|
||||||
|
- **并发限流** `kMaxConcurrent=12`(请求队列 `netQueue_`/`pumpNetQueue`)。
|
||||||
|
- **缓存**:影像纹理 `texCache_` + DEM `demCache_`,跨隐藏/重选保留 → 重选秒出。
|
||||||
|
- **合并渲染** `requestRender()`(Qt 队列合并同轮多次渲染请求为一帧;并在渲染前 `ResetCameraClippingRange`)。
|
||||||
|
- **动态范围**:底图最大距离 = 剖面合并范围半径×10,夹 [2km,30km],随勾选增删自动伸缩(`dataRadiusProvider_` 查 `VtkSceneView::dataHorizontalRadius()`)。**超过范围的粗瓦也强制细分**(否则一块巨瓦盖住中心、绕过距离剔除)。
|
||||||
|
- **地形半透明 0.55**(`kTerrainOpacity`)——地下剖面可从任意角度透过地面看到(地球物理标准做法;解决"前后左右预设看不到剖面=不透明地形遮挡地下")。
|
||||||
|
- **近裁剪容差 1e-5**(`SetNearClippingPlaneTolerance`,VtkSceneView 构造)——远处底图把近裁剪面顶出去会切掉近处剖面,调小修复。
|
||||||
|
- **就近优先加载**(离相机近的瓦片先拉)。
|
||||||
|
- 瓦片纹理 mipmap + 各向异性 16x + edgeClamp。
|
||||||
|
|
||||||
|
**剖面垂直配准 + VE**:
|
||||||
|
- 剖面 Z = **`+g.y`(真实高程)**——与原版一致(实证 `threeMap.vue:676`:剖面世界竖向 = data.y)。地形也用真实高程 → 同系对齐、剖面顶≈地表露出。(早期错把 y 当深度 `-y`,已改。)
|
||||||
|
- 垂直夸张默认 **1.0**,收敛为**单一来源** `kVerticalExaggeration`(main.cpp),下发控制器/底图/UI。剖面与地形用同一 VE。
|
||||||
|
|
||||||
|
**增量渲染**(`VtkSceneController` + `VtkSceneView`):
|
||||||
|
- 勾选/取消 = **按 dsId 增删图元**,不再整场 clear + 全量重建;`clear()` 保留底图;增量不重置相机(视角不跳);首批数据/全量重建才 `fitView`。
|
||||||
|
- `computeDataBounds()` 只算数据图元(不含底图)→ 坐标轴/取景/预设(前后左右)不被公里级底图撑大/推远。
|
||||||
|
- `onCameraChanged` 回调:相机程序化变化(取景/预设/缩放)后通知底图按新视锥重算覆盖(治"首帧部分瓦片要微动才出")。
|
||||||
|
- 默认底图=天地图;首个剖面重锚 frame 后经 `onFrameReanchored` 在数据位置加载底图。
|
||||||
|
- VTK 全屏含左侧三栏(drawer 在 vtkDock 内 + 进入全屏展开)。
|
||||||
|
|
||||||
|
## 3. 当前状态
|
||||||
|
- 底图/地形/剖面配准/增量渲染:**完成且可用**。编译绿。所有改动已提交到 `feat/vtk-3d-view`。
|
||||||
|
- **#1 客户端「生成三维体」流程:已实现,编译链接绿(exit 0),未提交、未 GUI 实测**(Claude 无法验 VTK 渲染,待用户启动 app 实测)。详见计划 `docs/superpowers/plans/2026-06-17-vtk-3d-volume-create-flow.md`。
|
||||||
|
- 已落地:`data::VolumeBuildParams`(不冻结 gridSpec,源 ds 锁定不变式);`core::buildVolume` 共享管线(LocalSample/Api 同源,消 TODO 漂移);`Api3dRepository` 内存体存储 + `createVolume/volumeRows/isVolumeDataset` + 多源 `loadVolume`(复用 `loadSection`,竖向=g.y 高程,与帘面构造性对齐);`loadVolume` 回调改交付 `(VolumeGrid, ColorScale)`(体色阶=源剖面色阶);`Column3DDataset`(源数据栏) 多选+右键「生成三维体」+ `VolumeParamsDialog`;**生成的体归三维分析栏**(`Column3DAnalysis`,设计 §2.1,非数据集栏),main.cpp `lastAnalysisRows`+`refreshAnalysis` 合并注入(体行不被后端刷新冲掉)+ **两栏勾选聚合** `pushChecked`(剖面+体/切片并集下发,避免互相清除);`VtkSceneController` 按 `isVolumeDataset` 分流体素/帘面(取代全局 showVoxel/showCurtain)。
|
||||||
|
- **待实测**:在**三维数据集栏**多选反演剖面→右键「生成三维体」→参数→新体行出现在**三维分析栏**→在三维分析栏勾选→渲染体;体是否与帘面对齐;色阶是否正确;剖面与体可同时勾选共存。
|
||||||
|
- 下一阶段(切片/异常 #2–#6):**仅完成设计定稿**,未开始编码。
|
||||||
|
|
||||||
|
## 4. 下一步计划(三维体 / 切片 / 异常)
|
||||||
|
**权威设计文档**:`docs/superpowers/specs/2026-06-17-vtk-3d-volume-slice-anomaly-design.md`(数据模型 + 交互流 + 后端vs mock + 代码现状 + 实现拆解 + 持久化策略)。已与用户拍板的关键决策:
|
||||||
|
- **创建三维体 = 客户端**:「三维数据集」栏多选剖面 → 选插值模型/参数 → `core::IdwInterpolator` 生成体素。
|
||||||
|
- **异常 = 接真实后端端点**(已从 web 源码挖到,见设计文档 §3):`POST/PUT/DELETE /business/exception` + `exceptionType/*` + `exceptionConsortium/*` + 读 `queryException*`/`queryExceptionTree`。`remarkSourceId/Type` 填切片数据集。
|
||||||
|
- **三维体网格 / 切片持久化 / 三维分析任务 = 后端无端点 → 先本地 mock**(保持 `I3dSceneRepository` 接口不变,端点就绪只换实现)。
|
||||||
|
- **持久化策略**(设计文档 §7):三维体保存时 **参数(源数据+插值模型/参数+色阶) + 网格规格 GridSpec 必存,明细 values 可选**;加载时有明细直接渲染、无明细按参数重算填入固定 GridSpec(锚定切片/异常坐标)。带切片/异常或大/慢的体建议存明细。
|
||||||
|
- 数据模型层级:**三维体(源数据+插值参数) → 切片(属于体,保存后成 dd_slice) → 异常(画在切片平面,圈定保存)**,三者皆可持久化。
|
||||||
|
|
||||||
|
**实现拆解(设计文档 §6,按依赖排序)**:
|
||||||
|
1. ~~三维体 mock 渲染~~ **✅ 已实现(编译绿,待 GUI 实测)**——见 §3 与计划 `2026-06-17-vtk-3d-volume-create-flow.md`。`Api3dRepository::loadVolume` 已接通(多源复用 loadSection → IDW → VolumeGrid + 色阶交付);`VolumeBuildParams` 必存参数、values 惰性重算+缓存(**不冻结 gridSpec**,改用源 ds 锁定不变式,留校验 TODO)。
|
||||||
|
2. 切片交互接通三维体(现有 `SliceTool`/`InteractionManager` 已能切;补滚轮推进、双击正视)。
|
||||||
|
3. 切片保存/另存/导出/删除(保存删除 mock 内存;导出图片/dat 客户端做)+ VTK 视图切片右键菜单接线。
|
||||||
|
4. 异常:切片右键创建异常(圈定+保存对话框含截图)→ **接真实端点**。
|
||||||
|
5. 分析栏右键菜单接线:色阶/显示隐藏(客户端)+ 切片增删(接 #3)。`Column3DAnalysis` 信号已定义,main.cpp 目前**只接了 `sliceRequested`+`detailRequested`**,其余未连。
|
||||||
|
6. 三维体/切片/异常详情面板(源数据/插值参数/色阶/测量点数体积/异常列表)。
|
||||||
|
|
||||||
|
**其它小项**:坐标轴「O点位置」「字体」弹框仍是 stub(main.cpp:382 TODO P4)。
|
||||||
|
|
||||||
|
## 5. 相关文档
|
||||||
|
- **`docs/superpowers/specs/2026-06-17-vtk-3d-volume-slice-anomaly-design.md`** ← 下一阶段主依据。
|
||||||
|
- `docs/questions/2026-06-16-反演剖面竖向字段(y-z-elevation)语义待确认.md` ← 已解:y=高程,z=+y 与原版一致;跨数据集 y 不一致是数据层问题(原版同样存在),非客户端 bug。
|
||||||
|
- `docs/questions/2026-06-17-3D地球改造-现状与约束评估.md` ← 已决:维持局部平面方案,不做真 3D 地球(opus 评审:球面=根本性重构)。
|
||||||
|
- 旧 spec/plans(`2026-06-15-vtk-3d-*`、`three-column-refactor*`)为历史,留档。
|
||||||
|
|
||||||
|
## 6. 关键铁律 / 坑(务必遵守)
|
||||||
|
- **构建**:`build.bat rebuild` 会**自动启动 app**(后台命令不返回);编译验证用 **`build.bat app`**。Claude 无法 GUI 验证 VTK 渲染。
|
||||||
|
- **不能靠猜**:渲染/裁剪类问题用**日志取证**(`%LOCALAPPDATA%\Geomative\Geopro3\logs\geopro_YYYYMMDD.log`,含 `[basemap]`/`[view]` 的 qInfo/qWarning),或读真实数据/原版源码。本会话多次因臆测被用户纠正——**贴源码/日志,别凭印象**。
|
||||||
|
- **勿动未跟踪文件**(见顶部);**勿用 `git add -A`**(会误纳)。逐个文件 `git add`。
|
||||||
|
- **API 凭证不得提交**(探测脚本用完即删)。
|
||||||
|
- 原版 web 源码 `D:\Git\lanbingtech\commercial-admin` 是复刻权威;需求 xlsx 用 `python openpyxl` 读到 UTF-8 临时文件再看(控制台中文乱码)。
|
||||||
|
- 内嵌 token:天地图 tk + Mapbox pk(公开客户端 token,同原版,可提交)。
|
||||||
- 全部回复中文。
|
- 全部回复中文。
|
||||||
|
|
||||||
## 3. 权威文档
|
## 7. 代码地图(关键文件)
|
||||||
- spec:`docs/superpowers/specs/2026-06-15-vtk-3d-supplementary-design.md`(v2,已纳入架构评审 + web 实地分析;§4 是补充需求逐行映射表;§6 接口设计;§14 分期)。
|
- `src/app/TileBasemap.{hpp,cpp}` — 底图+地形(四叉树/剔除/限流/缓存/动态范围/半透明/合并渲染)。
|
||||||
- 计划:`plans/2026-06-15-vtk-3d-p1-revive-rendering.md`、`plans/2026-06-15-vtk-3d-p2-dataset3d-bar.md`、`plans/2026-06-16-vtk-3d-p3-slice-interaction.md`。
|
- `src/app/VtkSceneView.{hpp,cpp}` — I3dSceneView 实现:clear/addCurtain(dsId)/addVolume(dsId)/removeDataset/render/renderIncremental/computeDataBounds/dataHorizontalRadius;近裁剪容差;onCameraChanged/onFrameReanchored 回调。
|
||||||
|
- `src/controller/VtkSceneController.{hpp,cpp}` — 增量渲染编排(setCheckedDatasets diff、addDatasetAsync、fitOnArrival)。
|
||||||
## 4. 已完成并经用户验收(commit 范围 faee28c..07f2f25)
|
- `src/render/actors/CurtainActor.cpp` — 帘面(Z=+g.y 真实高程)。
|
||||||
- **P1 复活渲染**(`0f521c5`+`53ccdc0`):`VtkSceneController`(编排,异步)+`I3dSceneRepository`/`LocalSample3dRepository`+`I3dSceneView`/`VtkSceneView`+`Scene::addViewProp`(体绘制 vtkVolume 入场)。勾选对象→样本数据→渲染帘面/体素/地形。
|
- `src/render/interact/{SliceTool,InteractionManager,SlicePlaneMath}.*` — 切片交互。
|
||||||
- **P2 三维数据集栏**(`3dea339`+样式 `73deb2b`/`86e0772`):坐标轴(标准/三维立体/不显示 vtkCubeAxesActor)、刻度(无/米/英尺/经纬度,GeoLocalFrame::toLatLon)、水平/垂直比例滑块、快捷视图6向、Zoom(In/Out/Fit)。右上工具条浮层(仅三维显示)。
|
- `src/render/{VoxelFromScatters,actors/VoxelActor}.*` + `src/core/algo/IdwInterpolator.*` — 体素插值/绘制(三维体复用)。
|
||||||
- **P3 切片交互**(`85d4ff5`..`07f2f25`):`src/render/interact/`(SlicePlaneMath/SliceTool/PickInteractorStyle/InteractionManager)。**已验收**:
|
- `src/data/repo/I3dSceneRepository.hpp` — 接口(loadVolume/createSlice/saveSlice/deleteSlice/loadAnomalyTree/saveAnomaly/.../loadTaskRecords)。
|
||||||
- 上下/前后/左右切片=固定角度可移动(G22-24);任意切片=拖边缘旋转(F25)+拖中间移动。
|
- `src/data/repo/LocalSample3dRepository.cpp`(内存 mock 参考实现)、`src/data/api/Api3dRepository.cpp`(真实路径,多为 stub `kNotReady`,待按设计文档实现)。
|
||||||
- 触碰切片→选中+**亮青边框**高亮(未选暗灰)。
|
- `src/app/panels/columns/{Column3DDataset,Column2DDataset,Column3DAnalysis,ColumnDrawer}.*` — 三栏 UI(信号定义全、main.cpp 未全接)。
|
||||||
- **双击切片→正视**(D40,靠 widget StartInteractionEvent 350ms 双击判定)。
|
- `src/app/main.cpp` — 装配/接线(搜 `basemap`/`colAnalysis`/`onCameraChanged`/`kVerticalExaggeration`)。
|
||||||
- 滚轮→推进**选中**切片(D46);关闭→移除选中;翻转(E55)。
|
- `src/data/dto/DatasetChartDto.cpp` — `parseInversionGrid`(x/y/v/lat/lon;**未解析 elevation/alt**)。
|
||||||
- **D39 以选中切片为中心旋转视图**:`PickInteractorStyle::Rotate()` 自定义——按下不动相机、拖动时绕选中切片中心(getRotateCenter)增量旋转整个相机(T(c)·R(up)·R(right)·T(-c))→不跳。
|
|
||||||
- **构建基建修复**(重要,见 §7):`build.bat` vswhere/ASCII/加 `rebuild`;`vcpkg.json` 加 `builtin-baseline`。
|
|
||||||
- ctest 全绿 **221/221**。
|
|
||||||
|
|
||||||
## 5. 当前 UI 是"过渡态",**不是 spec A1 的三栏**(已与用户讲明,属"功能先行、结构后做"的有意分期)
|
|
||||||
现状 = 旧「二维地图/三维视图」切换 + 三个浮层/工具条:
|
|
||||||
- 左上「视图详情」(layerPanel):帘面/体素/地形 复选框;
|
|
||||||
- 右上「三维数据集栏」(axisBar):坐标轴/刻度/比例/快捷视图/Zoom;
|
|
||||||
- 左下「切片」(sliceBar):上下/前后/左右/任意/翻转/关闭。
|
|
||||||
渲染由 LocalSample 样本驱动:对象树勾选任意 TM → 映射成样本 ds `"grid1"`(main.cpp checkedTmsChanged 处)。
|
|
||||||
|
|
||||||
## 6. 下一步(用户即将定方向;我已建议先做①)
|
|
||||||
**① 三栏结构重构(建议先做,A1 顶层框架)**:把界面重组为 三维数据集 / 二维数据集 / 三维分析 三栏,各栏含:
|
|
||||||
- 数据集列表(按 ds 维度 3D/2D 过滤勾选对象的 ds,C9/C15;维度映射见 `I3dSceneRepository::dimensionOf`);
|
|
||||||
- 三维分析栏的树(按 对象/三维体模型/切片 结构,C19);
|
|
||||||
- **右键菜单创建切片**(D21/F22-25:右键三维体→上下/前后/左右/任意切片;现在是用左下浮层按钮代替,须改成右键)。
|
|
||||||
**② P4 功能**(结构之后或并行):
|
|
||||||
- 切片 CRUD:保存/保存为/导出图片/导出dat/删除为数据集(F30-33/D47/E51-53)——`I3dSceneRepository` 已留 SliceSpec/createSlice 等接口位(spec §6.3),本轮内存态。
|
|
||||||
- 创建异常+异常体管理(D48/E49-50/A69-C88):异常=切片面上的 2D 多边形(复用 core::Anomaly);异常体树/删除/属性/VTK↔列表联动/显示过滤/截图属性。
|
|
||||||
- 三维体/切片详情(A58-C67):源数据/切片/异常/插值模型(IDW·克里金)/参数/色阶/测量(点数·体积);切片详情参照 dd_section。
|
|
||||||
- 任务管理(A90-C92):任务记录 + 可使用任务列表(按 ds 类型过滤 model/list)。
|
|
||||||
**③ P5 二维数据集栏**:底图(天地图/Google/隐藏)+2D视图位置(关闭/Z=0/顶部/底部/自定义Z)。VTK 瓦片层+EPSG:3857→GeoLocalFrame 配准(复用 TerrainActor 流程)。
|
|
||||||
- 待精修(需 Geopro **1.0** 实地参考,目前无):F26 色阶"参考1.0"、F50 异常保存框"参考1.0"。
|
|
||||||
|
|
||||||
## 7. ⚠️ 构建/验证铁律(务必遵守,否则重蹈本会话覆辙)
|
|
||||||
- 构建用 `build.bat`(已修好)。从 Git Bash 调:`cmd.exe /c "chcp 65001 >nul && cd /d D:\Git\lanbingtech\geopro && .\build.bat test"`。命令:`app`/`run`/`test`/`rebuild`(--clean-first 强制全量重编+启动)/`configure`。
|
|
||||||
- **ninja 增量偶发漏编** → 改了代码却"看不到效果"时,用 `build.bat rebuild` 或 `touch` 改过的源再编;验 exe 新鲜:`stat -c '%y' build/release/src/app/geopro_desktop.exe`。
|
|
||||||
- **切勿 `rm -rf build/release`**(vcpkg.json 虽已加 baseline,但重配会从源码重编 openssl/gdal/proj,慢;增量链接错用 `--clean-first`,别删目录)。
|
|
||||||
- **.bat 必须纯 ASCII**(中文 Windows cmd 按 GBK 解析 .bat,UTF-8 中文注释会让解析崩)。
|
|
||||||
- **Claude 工具跑 build 会间歇被一个 `Start-Process 'C:\Users\corey\...'` 钩子劫持**(环境问题、非项目;只影响 Claude 工具,不影响用户终端)→ 我的构建验证有时静默没跑,会误判"已更新";**交互类改动必须让用户在其终端 `build.bat rebuild` 实测**。
|
|
||||||
- **Claude 无法 GUI 测试**:VTK 交互(切片/旋转/拾取)的正确性只能靠用户实测——纯逻辑(几何/相机数学)抽成单测,widget 行为靠目视。
|
|
||||||
- app 启动需登录(真实 API,tenant.geomative.cn);勾"记住登录(30天)"可免登。
|
|
||||||
- 详见记忆 [[build-vs2026-vcpkg-gotchas]]、[[build-run-verify-gotchas]]、[[build-ninja-stale-shared-header]]。
|
|
||||||
|
|
||||||
## 8. 代码地图
|
|
||||||
- `src/render/actors/`:Scatter/GridContour/Voxel(GPU体绘制)/Anomaly(2D)/Terrain/Curtain/MapLine/Electrode/**AxesActor**(P2)。
|
|
||||||
- `src/render/`:Scene(addActor/addViewProp/clear=RemoveAllViewProps)、CameraPreset(Top2D/Free3D/applyView6向/zoomBy/fitView)、VoxelFromScatters、ColorLutBuilder、ContourBands。
|
|
||||||
- `src/render/interact/`(P3):SlicePlaneMath(纯几何,有单测)、SliceTool(封 vtkImagePlaneWidget;轴向 SetPlaneOrientationTo*+MarginSize0 禁旋转;任意 Origin/Pt1/Pt2 45°可旋转;SetLeftButtonAction(SLICE_MOTION);onInteract 选中回调)、PickInteractorStyle(自定义 Rotate 绕支点+滚轮+手动双击)、InteractionManager(切片增删/选中/滚轮/翻转/双击正视/getRotateCenter)。
|
|
||||||
- `src/controller/`:VtkSceneController(QObject 编排,异步回调 QPointer+generation 守护)、I3dSceneView(抽象,解耦 VTK)。
|
|
||||||
- `src/data/repo/`:I3dSceneRepository(异步接口:dimensionOf/loadVolume(VolumeGrid)/loadTerrainPaths,切片/异常/任务签名留位)、LocalSample3dRepository、IDatasetRepository、LocalSampleRepository。
|
|
||||||
- `src/app/`:VtkSceneView(I3dSceneView 实现)、main.cpp(buildWorkbench 内全部接线 + 三个浮层/工具条 + InteractionManager 创建于 ~309)。
|
|
||||||
- 测试:`tests/render/`(test_scene/test_camera_preset/test_axes/test_slice_plane_math)、`tests/data/`(test_3d_repo)、`tests/controller/`(test_vtk_scene_controller)。
|
|
||||||
|
|
||||||
## 9. 工作方式(用户偏好)
|
|
||||||
- 派 opus subagent 实现 → 完成后**我亲自**独立验证构建/测试 + 派 cpp-reviewer 审查 + 查 code 与 spec 符合度 → 修 CRITICAL/HIGH。
|
|
||||||
- 不要嘴上保证"编进去了"——用证据(ctest 输出、exe mtime、读改动文件)说话。
|
|
||||||
- 抠准每条需求(本会话因没抠准 G22-24/F25/D40 反复返工,务必先逐条读「补充需求」对应行)。
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
# 实现计划:客户端「生成三维体」完整流程(#1)
|
||||||
|
|
||||||
|
- 日期:2026-06-17
|
||||||
|
- 分支:`feat/vtk-3d-view`
|
||||||
|
- 上位设计:`docs/superpowers/specs/2026-06-17-vtk-3d-volume-slice-anomaly-design.md`(§2.4 客户端创建、§6 拆解、§7 持久化策略)
|
||||||
|
- 决策:用户已拍板「完整生成三维体客户端流程」(非最小 mock)。
|
||||||
|
- 原则:缺后端端点 → 内存 mock,保持 `I3dSceneRepository` 接口可替换;客户端能做的先做。
|
||||||
|
|
||||||
|
## 0. 现状取证(file:line)
|
||||||
|
|
||||||
|
| 事实 | 证据 |
|
||||||
|
|---|---|
|
||||||
|
| 运行路径用 `Api3dRepository`(非 LocalSample) | `main.cpp:254` |
|
||||||
|
| `Api3dRepository::loadVolume` 是 stub | `Api3dRepository.cpp:57-60` |
|
||||||
|
| 控制器→loadVolume→addVolume 管线已接好,但 voxel 路径靠全局 `showVoxel_=false` 且无 UI 触发 | `VtkSceneController.cpp:70-89`、`VtkSceneController.hpp:70` |
|
||||||
|
| 取源散点最干净路径 | `dsRepo_.loadAsync("inversion.scatter", dsId)` → `core::ScatterPayload`(`ApiDatasetRepository.cpp:152-165`;散点含 projX/projY + z(hlist=高程) + v,`DatasetChartDto.cpp:39-46`) |
|
||||||
|
| 帘面/地形竖向 = +elevation(Z=+g.y) | 交接文档 §2;`CurtainActor.cpp` |
|
||||||
|
| LocalSample loadVolume 参考实现(散点→配准→GridSpec→IDW),但用 `-s.y` 深度且固定样本 | `LocalSample3dRepository.cpp:68-147` |
|
||||||
|
| 散点→GridSpec→IDW 在 LocalSample/Api 重复,已有 TODO 标记漂移风险 | `LocalSample3dRepository.cpp:31-37` |
|
||||||
|
| 数据集列表后端全量加载、每次勾选测线变化全量覆盖 | `main.cpp:626-665`;`DatasetListPanel.cpp:187-216`(append=false) |
|
||||||
|
| `DsRow` 字段 | `RepoTypes.hpp:10-15`(id/dsName/typeName/ddCode/parentId/...) |
|
||||||
|
| 维度分流 dd_voxel→Dim3D | `DatasetDimension.cpp:8-15`;`Api3dRepository::dimensionOf` |
|
||||||
|
| Column3DDataset 列表是 checkbox 勾选(渲染),无多选/右键 | `Column3DDataset.cpp:114-128` |
|
||||||
|
| col3D 信号接线 | `main.cpp:362-386` |
|
||||||
|
|
||||||
|
## 1. 数据模型:VolumeBuildParams(设计 §7.4)
|
||||||
|
|
||||||
|
新增 `src/data/repo/VolumeBuildParams.hpp`(贴近消费它的 `I3dSceneRepository`/`Api3dRepository`):
|
||||||
|
```cpp
|
||||||
|
struct VolumeBuildParams {
|
||||||
|
std::vector<std::string> sourceDatasetIds; // 源数据集 id(≥1)
|
||||||
|
enum class Model { Idw, Kriging };
|
||||||
|
Model interpModel = Model::Idw;
|
||||||
|
double cellXY = 1.0, cellZ = 0.5, power = 2.0, maxDist = 4.0; // interpParams
|
||||||
|
std::string colorScaleId; // 取哪个源的色阶(默认首个源)
|
||||||
|
double vmin = 0.0, vmax = 0.0; // 派生(缓存)
|
||||||
|
struct Measure { long points = 0; double volume = 0.0; } measure; // 派生(缓存)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
> **不冻结 gridSpec**(用户决策 2026-06-17):gridSpec 每次从源散点**确定性重算**(IDW 确定 + 源锁定 → 结果必然一致),不存为"冻结锚点"。
|
||||||
|
> 切片/异常坐标的稳定性由**源 ds 锁定**不变式保证(见 §9),而非冻结派生网格。
|
||||||
|
> 若三维体变了 ⇒ 源 ds 被动过 ⇒ 旧 gridSpec 本身已失效,冻结只会掩盖问题。
|
||||||
|
|
||||||
|
## 2. 共享管线:core::algo(解决 TODO 漂移)
|
||||||
|
|
||||||
|
新增 `src/core/algo/VolumeBuilder.{hpp,cpp}`(纯 core,不碰 CRS):
|
||||||
|
```cpp
|
||||||
|
struct BuiltVolume { core::ScalarVolume vol; core::GridSpec spec; double vmin, vmax; };
|
||||||
|
// 入参:世界局部米点集 + cell/power/maxDist。
|
||||||
|
// 包络→GridSpec(角点对齐, clampDim)→IDW→ScalarVolume→vmin/vmax(优先外部色阶 stops,否则实测)。
|
||||||
|
// 确定性:同点集同参数 → 同 GridSpec 同体(不需冻结,见计划 §1 决策)。
|
||||||
|
BuiltVolume buildVolume(const core::PointSet& pts, double cellXY, double cellZ,
|
||||||
|
double power, double maxDist);
|
||||||
|
```
|
||||||
|
- 把 `LocalSample3dRepository.cpp:95-137` 的「包络→GridSpec→IDW→vmin/vmax」逻辑提进来。
|
||||||
|
- `LocalSample3dRepository::loadVolume` 改为调用它(行为不变,回归保护)。
|
||||||
|
- `clampDim`/kMaxDim 一并移入。
|
||||||
|
|
||||||
|
## 3. Api3dRepository:体存储 + loadVolume + createVolume(已实现)
|
||||||
|
|
||||||
|
构造注入:`Api3dRepository(IAsyncDatasetRepository& dsRepo, std::shared_ptr<core::GeoLocalFrame> frame)`。
|
||||||
|
- **只需 frame**(与帘面/底图同一共享 shared_ptr,含 reanchor);**不需 projectCrs/refElev**——因取源走 `loadSection` 的 `Grid`,其节点已带 lat/lon + 高程轴 g.y,无须 CRS 正变换。
|
||||||
|
|
||||||
|
内存 mock 存储(成员):
|
||||||
|
```cpp
|
||||||
|
struct StoredVolume { VolumeBuildParams params; std::string name; std::optional<VolumeGrid> cachedGrid; };
|
||||||
|
std::map<std::string, StoredVolume> volumes_; // dsId → 体
|
||||||
|
int volumeCounter_ = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
新增公有方法(concrete,main.cpp 持具体指针调用):
|
||||||
|
- `std::string createVolume(VolumeBuildParams params, const std::string& name)`:生成 `vol-N` dsId、存入 `volumes_`、返回 id(不立即插值;插值在首次 loadVolume 惰性做 + 缓存)。
|
||||||
|
- `std::vector<DsRow> volumeRows() const`:转 `DsRow{id, dsName=name, ddCode="dd_voxel", typeName="三维体"}`,供列表合并。
|
||||||
|
- `bool isVolumeDataset(const std::string& dsId) const`:`volumes_.count`。
|
||||||
|
|
||||||
|
`loadVolume(dsId)` 实现(异步 N 源扇出,**复用 loadSection 保证与帘面同系对齐**):
|
||||||
|
1. 查 `volumes_[dsId]`;无 → onErr。
|
||||||
|
2. 有 `cachedGrid` → 直接 onOk(明细命中)。
|
||||||
|
3. 否则:对每个 sourceId 调 `this->loadSection(srcId)`(即 `inversion.grid` 路径),`shared_ptr<Agg>` 聚合 N 个回调(全到齐再继续;任一 failed → 整体 onErr 一次)。
|
||||||
|
4. `appendGridPoints`:对每个源 Grid 的**有限值**节点,按 CurtainActor 同口径定位 →
|
||||||
|
`frame.toLocal(g.lat[i], g.lon[i])` 作 (x,y);**世界 Z = g.y[j](高程)**;v = g.valueAt(i,j)。跳过 NaN 格(与帘面消隐一致)。
|
||||||
|
5. 取首个到达源的色阶定 vmin/vmax(否则 buildVolume 数据实测)。
|
||||||
|
6. `core::buildVolume(pts, cellXY/cellZ/power/maxDist)`(每次从源确定性重算 gridSpec)。
|
||||||
|
7. `cachedGrid` 缓存;onOk(VolumeGrid)。
|
||||||
|
|
||||||
|
> 契约:onOk/onErr 主线程回调。`DetailLoad::done` 在主线程发,扇出聚合用主线程共享 `Agg`(无锁)。
|
||||||
|
> 路径选择说明:弃用 `inversion.scatter` 端点(其 y=深度/z=高程语义是交接文档警告的坑);
|
||||||
|
> 改复用已正确的 `loadSection`(`inversion.grid`),与帘面**构造性对齐**,且免 CRS 正变换。
|
||||||
|
|
||||||
|
`I3dSceneRepository` 接口加纯虚 `virtual bool isVolumeDataset(const std::string& dsId) const = 0;`(LocalSample 恒 false)。
|
||||||
|
|
||||||
|
## 4. 控制器:按类型分流 curtain vs voxel
|
||||||
|
|
||||||
|
`VtkSceneController::addDatasetAsync`(`VtkSceneController.cpp:49-90`)改:
|
||||||
|
- `if (sceneRepo_.isVolumeDataset(dsId))` → 走 loadVolume/addVolume 路径;
|
||||||
|
- `else` → 走 loadSection/addCurtain 路径。
|
||||||
|
- 移除对全局 `showVoxel_`/`showCurtain_` 作为分流条件的依赖(保留成员或删,二者均不再 gating 单 ds 分流;图层开关语义后续再议)。
|
||||||
|
|
||||||
|
## 5. UI:Column3DDataset(源数据栏)多选 + 右键「生成三维体」
|
||||||
|
|
||||||
|
> 归属修正(2026-06-17,用户指出):**源选择**在三维数据集栏(剖面池);**生成的体**进**三维分析栏**(§7)。
|
||||||
|
|
||||||
|
`Column3DDataset.{hpp,cpp}`(源数据栏):
|
||||||
|
- 列表 `setSelectionMode(ExtendedSelection)`(多选高亮,独立于 checkbox 渲染勾选)。
|
||||||
|
- `setContextMenuPolicy(CustomContextMenu)` + 槽:右键弹菜单「生成三维体」,仅当选中项 ≥1 且均为可作源的 ddCode(dd_section/dd_inversion_data)时启用。
|
||||||
|
- 新信号 `void generateVolumeRequested(const QStringList& sourceDsIds);`(取选中项的 kDsIdRole)。
|
||||||
|
|
||||||
|
## 6. UI:插值参数对话框
|
||||||
|
|
||||||
|
新增 `src/app/VolumeParamsDialog.{hpp,cpp}`(QDialog,与现有对话框同放 app 根目录):
|
||||||
|
- 名称、插值模型(IDW;克里金项 disabled 占位)、cellXY/cellZ/power/maxDist(QDoubleSpinBox,默认同 §1)。
|
||||||
|
- `accept` → `volumeName()` / `params()`(不含 sourceDatasetIds,由调用方填)。
|
||||||
|
|
||||||
|
## 7. 装配:main.cpp 接线(体→三维分析栏 + 两栏勾选聚合)
|
||||||
|
|
||||||
|
- main.cpp 改 `Api3dRepository` 构造,传 `frame`(shared_ptr,§3 不需 projectCrs/refElev)。
|
||||||
|
- **体归三维分析栏**:`lastAnalysisRows` 缓存后端 Analysis 行(dd_slice);`refreshAnalysis()` = `colAnalysis()->setDatasets(lastAnalysisRows + scene3dRepo->volumeRows())`。三维数据集栏(col3D)恢复直接 `setDatasets(b.dim3D)`(仅后端剖面,源池)。
|
||||||
|
- **渲染勾选聚合**:`checkedProfiles`(col3D 勾选剖面) + `checkedAnalysis`(colAnalysis 勾选体/切片) → `pushChecked()` 并集下发 `setCheckedDatasets`(控制器全量 diff,必须并集,否则一栏清掉另一栏)。
|
||||||
|
- 连接 `c3.generateVolumeRequested` → `VolumeParamsDialog` → 填 `params.sourceDatasetIds` → `scene3dRepo->createVolume` → `refreshAnalysis()`(新体出现在三维分析栏)。
|
||||||
|
- 连接 `c3.checkedDatasetsChanged`→更新 checkedProfiles;`ca.checkedItemsChanged`→更新 checkedAnalysis;均 `pushChecked()`。
|
||||||
|
- 后端加载回调:col3D 直接 `setDatasets(b.dim3D)`;analysis 经 `refreshAnalysis()`,保证体在测线重勾后仍驻留。
|
||||||
|
|
||||||
|
## 8. 阶段与验收(每阶段编译绿 = build.bat app)
|
||||||
|
|
||||||
|
- **阶段 A(模型+管线+loadVolume)**:§1 §2 §3。可单测 `buildVolume`;loadVolume 单源/多源逻辑成形。无 UI,不可见但编译绿 + 回归 LocalSample 不变。**✅ 编译绿**
|
||||||
|
- **阶段 B(创建流 UI)**:§5 §6 §7。可见:数据集栏多选剖面→右键生成→参数→新体行出现在**三维分析栏**→勾选渲染体。**✅ 编译绿**
|
||||||
|
- **阶段 C(分流)**:§4。dd_voxel 渲染为体、dd_section 渲染为帘面,二者可共存。**✅ 编译绿**
|
||||||
|
- 验收:用户启动 app 实测(Claude 无法 GUI 验证 VTK,按交接铁律用 `build.bat app` 仅编译验证;渲染问题查 `%LOCALAPPDATA%\Geomative\Geopro3\logs`)。
|
||||||
|
|
||||||
|
## 9. 风险 / 待确认
|
||||||
|
|
||||||
|
- **散点端点对反演剖面是否真有 hlist(高程)**:若某些数据 z 为空,竖向退化。落地时加日志取证,必要时回退用 grid.elevation(loadSection 路径)。
|
||||||
|
- **克里金**:UI 占位 disabled,仅 IDW 实现(设计 §1.1 列了克里金,但 core 仅 IdwInterpolator)。
|
||||||
|
- **mock 持久化形态**:本期纯内存(重启丢失),符合设计 §5「次要待确认」;本地文件持久化留后续。
|
||||||
|
- **源 ds 锁定不变式**(替代 gridSpec 冻结,用户决策):被三维体引用的源 ds **不可修改/删除**——保证 IDW 重算确定一致、切片/异常坐标稳定。
|
||||||
|
- 本期:内存 mock,仅留 TODO(在源 ds 删除/编辑入口校验"是否被某三维体引用,是则禁止/告警")。
|
||||||
|
- 推论:不冻结 gridSpec;若源真被改 ⇒ 体应失效重建,而非套旧锚点(旧锚点已是脏数据)。
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
# VTK 三维:三维体 / 切片 / 异常 —— 数据模型与客户端交互设计
|
||||||
|
|
||||||
|
- 日期:2026-06-17
|
||||||
|
- 范围:桌面客户端 VTK 三维视图里的「三维体模型(体素) / 切片 / 异常」三类数据及其交互。是补充需求页"三维分析"栏的落地设计。
|
||||||
|
- 依据:①《Geopro3.0 需求表.xlsx》「补充需求」页(行号见引用);② 与产品方就 6 个设计问题的确认;③ 现有代码。
|
||||||
|
- 原则:缺后端端点的**先本地 mock**(保证功能可见可用),端点就绪后切真实;能纯客户端做的先做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 核心数据模型
|
||||||
|
|
||||||
|
三者都是**可持久化的数据集**,层级关系:`三维体 → 切片 → 异常`。
|
||||||
|
|
||||||
|
### 1.1 三维体(VolumeModel)
|
||||||
|
ddCode:`dd_voxel` / `dd_Structual3D` / `dd_Property3D`(需求行 245/290/291)。
|
||||||
|
- `id` / `name`
|
||||||
|
- **`sourceDatasetIds[]`(源数据)**:一组输入数据集,用于插值生成本体(需求行 401)。
|
||||||
|
- **`interpModel`**:插值模型,克里金 / IDW(需求行 404)。
|
||||||
|
- **`interpParams`**:插值参数(cellXY/cellZ/power/maxDist 等,需求行 405)。
|
||||||
|
- `colorScale`(色阶参数,行 406)。
|
||||||
|
- `grid`:体素网格 = `ScalarVolume` + `origin/spacing/vmin/vmax`(= 现有 `VolumeGrid`)。
|
||||||
|
- `measure`:测量数据,点数/体积(行 407,派生量)。
|
||||||
|
- 派生:`slices[]`、`anomalies[]`(汇总该体下所有切片圈定的异常,行 403)。
|
||||||
|
|
||||||
|
**来源(行业经验)**:① 多条 2D 反演剖面/散点/钻孔点 → IDW/克里金插成 3D 体(最常见);② 直接 3D 采集(三维高密度等,本身即 3D,单数据集)。`sourceDatasetIds` 为 1 个或多个。
|
||||||
|
|
||||||
|
### 1.2 切片(Slice)
|
||||||
|
ddCode:`dd_slice`。
|
||||||
|
- `id` / `name`
|
||||||
|
- **`parentVolumeId`**:所属三维体(行 389:切片对象=所属三维体模型)。
|
||||||
|
- **`plane`**:切面 = `SliceSpec`(轴向/原点/法向/偏移)。
|
||||||
|
- `colorScale`。
|
||||||
|
- 生命周期:交互式"切片工具"是**临时**的(在体上实时切);**保存后才成为持久化的 `dd_slice` 数据集**,进入三维分析栏列表(行 390)。
|
||||||
|
|
||||||
|
### 1.3 异常(Anomaly)
|
||||||
|
- `id` / `name`
|
||||||
|
- **`parentSliceId`**:异常画在某切片平面上、附着于该切片(行 392 创建异常在切片右键)。
|
||||||
|
- `polygon`:圈定多边形(切片平面内的坐标)。
|
||||||
|
- `screenshot` + 元信息(截图大小、异常坐标,行 393)。
|
||||||
|
- 向上归属:三维体详情的"异常"= 汇总该体下所有切片圈定的异常(行 403)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 客户端交互流(需求"三维分析"栏 + VTK 视图右键)
|
||||||
|
|
||||||
|
### 2.1 三维分析栏(列表)
|
||||||
|
- 按「**对象 / 三维体模型 / 切片**」三级树显示(行 366)。
|
||||||
|
- 勾选一个或多个三维体或切片 → 显示在三维视图(行 367)。
|
||||||
|
- **三维体右键**(行 368–375):切片▸(上下/前后/左右/任意)、色阶、显示/隐藏、数据详情。
|
||||||
|
- **切片右键**(行 376–383):保存、保存为、导出、删除、色阶、显示/隐藏、数据详情。
|
||||||
|
|
||||||
|
### 2.2 切片工具(行 369–372, 387–389)
|
||||||
|
- 上下/前后/左右切片:在三维体中心或光标位置打开**水平切片工具,角度不可调**。
|
||||||
|
- 任意切片:初始角度 = 当前视图 45°,可任意调整。
|
||||||
|
- 选中切片滚轮 → 沿法向往内/外推进;切片对象 = 所属三维体(行 389)。
|
||||||
|
- 双击切片 → 视角调正为正视该切片(行 386/388)。
|
||||||
|
|
||||||
|
### 2.3 VTK 视图里对切片的右键(行 391–399)
|
||||||
|
- **创建异常**:弹异常工具,以光标拾取点为起点圈定;结束保存时弹对话框(截图大小、异常坐标),参考 Geopro1.0。
|
||||||
|
- **保存**:保存为数据集(切片数据集)。
|
||||||
|
- **导出为图片** / **导出到 dat**。
|
||||||
|
- **正视图** / **视图翻转(水平 180°)** / **关闭**。
|
||||||
|
|
||||||
|
### 2.4 创建三维体(**已定:客户端创建**)
|
||||||
|
产品确认创建三维体在**客户端**侧。
|
||||||
|
- 「三维数据集」栏多选若干 3D 数据集(反演剖面)→ 菜单「生成三维体」→ 选插值模型/参数 → 客户端 `IdwInterpolator` 插值 → 三维体。
|
||||||
|
- 持久化:后端**无三维体端点**(见 §3)→ 先本地 mock(内存/本地文件),端点就绪后切真实。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 后端依赖 vs 本地 mock(已核对 web 源码 `commercial-admin/src/apis`)
|
||||||
|
|
||||||
|
| 能力 | 现有端点 | 方案 |
|
||||||
|
|---|---|---|
|
||||||
|
| 展示已有三维体(取网格) | ❌ 无(`/model/*` 只有 ResIPy 反演/雷达,无三维体网格) | **mock**:以源数据集散点 IDW 出体(`IdwInterpolator`) |
|
||||||
|
| 创建三维体(插值) | ❌ 无(无三维空间插值端点) | **客户端做**(IdwInterpolator);持久化 mock |
|
||||||
|
| 三维体/切片**持久化** | ❌ 无写端点 | **mock**:内存/本地存;端点就绪后切真实 |
|
||||||
|
| 切片创建/交互 | — 纯客户端 | ✅ 已有(`SliceTool`/`InteractionManager`) |
|
||||||
|
| 切片保存/另存/导出/删除 | ❌ 无切片端点 | 保存/删除 → mock(内存);导出图片/dat → **客户端做**(截图/写文件) |
|
||||||
|
| 异常**读取** | ✅ `POST /exception/queryExceptionTree`、`queryException`/`queryExceptionByTmObjectId` | **接真实** |
|
||||||
|
| 异常**新增** | ✅ `POST /business/exception` | **接真实** |
|
||||||
|
| 异常**更新** | ✅ `PUT /business/exception` | **接真实** |
|
||||||
|
| 异常**删除** | ✅ `DELETE /business/exception/{exceptionId}` | **接真实** |
|
||||||
|
| 异常类型 | ✅ `GET /exceptionType/queryExceptionTypeByProjectIdAndType/{projectId}/{remarkSourceType}`、`/exceptionType/getDetail/{id}` | **接真实** |
|
||||||
|
| 异常体(exceptionConsortium) | ✅ `POST/PUT /exceptionConsortium`、`/page`、`/export`、`/getDetail/{id}` | **接真实** |
|
||||||
|
| 任务记录/可用任务 | ❌(`/model/task/page` 是反演任务,非三维分析任务) | mock |
|
||||||
|
|
||||||
|
### 异常新增请求体(web 实证 `datasetInfo/index.js:212`)
|
||||||
|
`POST /business/exception`:
|
||||||
|
```
|
||||||
|
{ exceptionName, exceptionTypeId, location:{...圈定坐标...},
|
||||||
|
projectId, remarkSourceId, remarkSourceType, remark }
|
||||||
|
```
|
||||||
|
- `remarkSourceId` / `remarkSourceType`:异常所附着的对象(本场景=切片数据集 id + 其类型)。
|
||||||
|
- `location`:圈定多边形坐标。截图:保存对话框含截图(行 393),上传方式参考 `exceptionInfos/modalNewException.vue`。
|
||||||
|
- 更新:`PUT /business/exception` `{ id, exceptionName, remark }`;删除:`DELETE /business/exception/{exceptionId}`;详情:`GET /business/exception/getDetail/{exceptionId}`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 代码现状(落点)
|
||||||
|
|
||||||
|
- 维度归类:`LocalSample3dRepository::dimensionOf`(dd_voxel/Structual3D/Property3D/section/inversion → 3D;dd_slice → Analysis)。
|
||||||
|
- 体素:`core::IdwInterpolator`(IDW 现成)、`render::buildVoxel`/`VoxelActor`(体绘制)、`VoxelFromScatters`(散点→体)。
|
||||||
|
- 切片交互:`render::interact::SliceTool` + `InteractionManager`(已可在体素 image 上切,含上下/前后/左右/任意)。
|
||||||
|
- 仓储接口:`I3dSceneRepository` 已定义 `loadVolume / createSlice / saveSlice / deleteSlice / loadAnomalyTree / saveAnomaly / deleteAnomaly / deleteAnomalyGroup / loadTaskRecords / loadUsableTasks`。
|
||||||
|
- `LocalSample3dRepository`:以上多为**内存 mock**(slices_/anomalies_ map)。
|
||||||
|
- `Api3dRepository`:以上多为 **stub(`kNotReady`)** — 真实数据未接。
|
||||||
|
- UI:`Column3DAnalysis` 已定义信号(sliceRequested/colorScaleRequested/visibilityToggled/detailRequested/sliceSave|SaveAs|Export|Delete);`main.cpp` **仅接了 sliceRequested + detailRequested**,其余未连。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 待产品确认(更新 2026-06-17)
|
||||||
|
|
||||||
|
1. **创建三维体在客户端还是平台?** → **已定:客户端**(§2.4)。
|
||||||
|
2. **后端三维体/切片/任务端点时间表?** → 暂未知。当前 web 源码**无**三维体网格、三维插值、切片、三维分析任务端点 → 这些**先 mock**(保持 `I3dSceneRepository` 接口不变,端点就绪后仅换实现)。
|
||||||
|
3. **异常写端点?** → **已找到**(§3):`POST/PUT/DELETE /business/exception` + 类型/异常体一整套 → 异常**接真实**,不 mock。
|
||||||
|
|
||||||
|
剩余待确认(次要):
|
||||||
|
- 异常保存对话框的**截图上传**方式/字段(参考 `exceptionInfos/modalNewException.vue`,落地时细化)。
|
||||||
|
- 三维体/切片本地 mock 的持久化形态(纯内存 vs 本地文件,影响重启后是否还在)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 实现拆解(对应"剩余工作" #2–#6,缺端点先 mock)
|
||||||
|
|
||||||
|
按依赖与价值排序:
|
||||||
|
|
||||||
|
1. **三维体(mock 渲染)**:把当前勾选/源数据反演剖面散点 → `IdwInterpolator` → `VolumeGrid` → `VoxelActor`。`Api3dRepository::loadVolume` 由 stub 改为"取源数据散点 + IDW"(mock)。→ 解锁三维体显示 + 切片有可切对象。
|
||||||
|
2. **切片交互接通三维体**:体素就位后,三维体右键"切片▸"已能创建切片(现有 SliceTool)。补:选中切片滚轮推进、双击正视。
|
||||||
|
3. **切片保存/另存/导出/删除**:保存/删除 → Api3dRepository 改内存 mock(同 LocalSample);导出图片(截图)/dat(写文件) → 客户端实现。VTK 视图切片右键菜单接线。
|
||||||
|
4. **异常**:切片右键"创建异常"→ 圈定工具 + 保存对话框(截图/坐标)。**全套接真实端点**:新增 `POST /business/exception`、更新 `PUT /business/exception`、删除 `DELETE /business/exception/{id}`、读 `queryException*`/`queryExceptionTree`、类型 `exceptionType/*`、异常体 `exceptionConsortium/*`。`remarkSourceId/Type` 填切片数据集。
|
||||||
|
5. **分析栏右键菜单接线**:色阶、显示/隐藏(纯客户端)、切片保存/另存/导出/删除(接 #3)。
|
||||||
|
6. **三维体/切片/异常详情**:数据详情栏展示源数据/插值参数/色阶/测量(点数·体积)/异常列表。
|
||||||
|
|
||||||
|
> 每步:客户端能做的先做、缺端点的内存 mock 留出可替换缝(保持 `I3dSceneRepository` 接口不变,仅换实现)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 三维体持久化策略与存储结构(2026-06-17 定)
|
||||||
|
|
||||||
|
### 7.1 策略:参数为准 + 明细可选缓存 + 缺则惰性重算
|
||||||
|
- **保存**:
|
||||||
|
- **必存**:插值参数(源数据引用 + 插值模型/参数 + 色阶)+ **网格规格 `GridSpec`**(origin/spacing/dims)。
|
||||||
|
- **可选存**:网格明细值(体素标量,nx·ny·nz)。
|
||||||
|
- **加载**:
|
||||||
|
- **有明细** → 直接渲染明细。
|
||||||
|
- **无明细** → 按参数把值**重算填入已存的 `GridSpec`**(坐标系固定),再渲染。
|
||||||
|
- **理由**:参数小且可复现(详情面板要展示);明细贵、但稳定一致;`GridSpec` 必存以**锚定切片/异常坐标**(它们定义在体网格坐标系上,重算必须落在同一规格里,否则错位)。
|
||||||
|
|
||||||
|
### 7.2 何时建议存明细
|
||||||
|
| 体的情形 | 建议 |
|
||||||
|
|---|---|
|
||||||
|
| 带切片/异常 | **存明细**(与源数据生命周期解耦,重算变形风险归零) |
|
||||||
|
| 网格大 / 插值慢 | **存明细**(避免每次加载实时重算的卡顿) |
|
||||||
|
| 纯展示、源数据稳定、网格小 | 可只存参数(省存储),加载时重算 |
|
||||||
|
|
||||||
|
### 7.3 重算路径的两个前提(UI/逻辑需兜住)
|
||||||
|
1. **源数据仍在且可取**:源数据集被删/改 → 重算失败或结果变 → 应回退提示 / 阻止。
|
||||||
|
2. **算法确定性**:同参数同输入 → 同结果(IDW 确定);算法升级时旧体优先用已存明细,避免变形。
|
||||||
|
|
||||||
|
### 7.4 存储结构(字段定义,驱动实现)
|
||||||
|
**三维体元数据(必存,进数据集元数据)`VolumeBuildParams`**:
|
||||||
|
- `sourceDatasetIds[]`:源数据集 id 列表。
|
||||||
|
- `interpModel`:枚举 IDW / Kriging。
|
||||||
|
- `interpParams`:`{ cellXY, cellZ, power, maxDist, ... }`(按模型)。
|
||||||
|
- `colorScaleId` 或内联色阶。
|
||||||
|
- `gridSpec`:`{ ox, oy, oz, dx, dy, dz, nx, ny, nz }`(**必存**,锚定坐标)。
|
||||||
|
- `vmin, vmax`、`measure{ points, volume }`(派生,可缓存)。
|
||||||
|
|
||||||
|
**三维体明细(可选,进数据集数据文件)**:
|
||||||
|
- `values`:`ScalarVolume`(nx·ny·nz 标量,maxDist 外为 NaN=空)。
|
||||||
|
|
||||||
|
**与现有代码的关系**:
|
||||||
|
- 现 `data::VolumeGrid = { ScalarVolume vol; origin[3]; spacing[3]; vmin; vmax }` ≈ "明细 + 规格(部分)"。
|
||||||
|
- 落地改造:拆出 `VolumeBuildParams`(含 `GridSpec`)为必存元数据;`vol`(values)为可选。`I3dSceneRepository::loadVolume` 返回时:若有 values 直接给 `VolumeGrid`;若无,则用 `VolumeBuildParams` 现场 `IdwInterpolator` 重算出 `vol` 再给(坐标用存好的 `gridSpec`)。
|
||||||
|
- mock 阶段:客户端 IDW 同时产出 params(含 spec) 与 values,两者本地存;接口签名不变,后端就绪后元数据走数据集属性、values 走数据文件。
|
||||||
|
|
@ -64,6 +64,7 @@ add_executable(geopro_desktop WIN32
|
||||||
ImportDatasetDialog.cpp
|
ImportDatasetDialog.cpp
|
||||||
ExportDatasetDialog.cpp
|
ExportDatasetDialog.cpp
|
||||||
SettingsDialog.cpp
|
SettingsDialog.cpp
|
||||||
|
VolumeParamsDialog.cpp
|
||||||
Logging.cpp
|
Logging.cpp
|
||||||
DatasetDimension.cpp
|
DatasetDimension.cpp
|
||||||
TileBasemap.cpp)
|
TileBasemap.cpp)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
#include "VolumeParamsDialog.hpp"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDoubleSpinBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 默认值与 data::VolumeBuildParams 同口径(保持单一真相)。
|
||||||
|
constexpr double kDefCellXY = 1.0;
|
||||||
|
constexpr double kDefCellZ = 0.5;
|
||||||
|
constexpr double kDefPower = 2.0;
|
||||||
|
constexpr double kDefMaxDist = 4.0;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
VolumeParamsDialog::VolumeParamsDialog(int sourceCount, QWidget* parent) : QDialog(parent) {
|
||||||
|
setWindowTitle(QStringLiteral("生成三维体"));
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
root->addWidget(new QLabel(
|
||||||
|
QStringLiteral("由 %1 个源数据集插值生成三维体").arg(sourceCount)));
|
||||||
|
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
|
||||||
|
name_ = new QLineEdit(QStringLiteral("三维体"));
|
||||||
|
form->addRow(QStringLiteral("名称"), name_);
|
||||||
|
|
||||||
|
model_ = new QComboBox();
|
||||||
|
model_->addItem(QStringLiteral("反距离加权 (IDW)"),
|
||||||
|
static_cast<int>(geopro::data::VolumeBuildParams::Model::Idw));
|
||||||
|
model_->addItem(QStringLiteral("克里金 (Kriging)"),
|
||||||
|
static_cast<int>(geopro::data::VolumeBuildParams::Model::Kriging));
|
||||||
|
// 克里金本期未实现(core 仅 IDW)→ 禁用该项,默认选 IDW。
|
||||||
|
if (auto* m = qobject_cast<QStandardItemModel*>(model_->model())) {
|
||||||
|
if (auto* it = m->item(1)) it->setEnabled(false);
|
||||||
|
}
|
||||||
|
model_->setCurrentIndex(0);
|
||||||
|
form->addRow(QStringLiteral("插值模型"), model_);
|
||||||
|
|
||||||
|
auto makeSpin = [this](double val, double min, double max, double step, int decimals) {
|
||||||
|
auto* s = new QDoubleSpinBox();
|
||||||
|
s->setRange(min, max);
|
||||||
|
s->setSingleStep(step);
|
||||||
|
s->setDecimals(decimals);
|
||||||
|
s->setValue(val);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
cellXY_ = makeSpin(kDefCellXY, 0.01, 1000.0, 0.5, 2);
|
||||||
|
cellZ_ = makeSpin(kDefCellZ, 0.01, 1000.0, 0.5, 2);
|
||||||
|
power_ = makeSpin(kDefPower, 0.5, 6.0, 0.5, 1);
|
||||||
|
maxDist_ = makeSpin(kDefMaxDist, 0.1, 10000.0, 1.0, 2);
|
||||||
|
form->addRow(QStringLiteral("水平间距 (米)"), cellXY_);
|
||||||
|
form->addRow(QStringLiteral("竖向间距 (米)"), cellZ_);
|
||||||
|
form->addRow(QStringLiteral("IDW 幂次"), power_);
|
||||||
|
form->addRow(QStringLiteral("最大影响距离 (米)"), maxDist_);
|
||||||
|
|
||||||
|
root->addLayout(form);
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
root->addWidget(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString VolumeParamsDialog::volumeName() const {
|
||||||
|
const QString n = name_->text().trimmed();
|
||||||
|
return n.isEmpty() ? QStringLiteral("三维体") : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
geopro::data::VolumeBuildParams VolumeParamsDialog::params() const {
|
||||||
|
geopro::data::VolumeBuildParams p;
|
||||||
|
p.interpModel = static_cast<geopro::data::VolumeBuildParams::Model>(model_->currentData().toInt());
|
||||||
|
p.cellXY = cellXY_->value();
|
||||||
|
p.cellZ = cellZ_->value();
|
||||||
|
p.power = power_->value();
|
||||||
|
p.maxDist = maxDist_->value();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "repo/VolumeBuildParams.hpp"
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
class QComboBox;
|
||||||
|
class QDoubleSpinBox;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 「生成三维体」参数对话框:名称 + 插值模型(IDW;克里金占位禁用)+ cellXY/cellZ/power/maxDist。
|
||||||
|
// sourceCount 仅用于提示文案("由 N 个源数据集插值生成")。
|
||||||
|
// accept 后经 volumeName()/params() 取结果;params() 不含 sourceDatasetIds(由调用方填充)。
|
||||||
|
class VolumeParamsDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit VolumeParamsDialog(int sourceCount, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
QString volumeName() const;
|
||||||
|
geopro::data::VolumeBuildParams params() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLineEdit* name_ = nullptr;
|
||||||
|
QComboBox* model_ = nullptr;
|
||||||
|
QDoubleSpinBox* cellXY_ = nullptr;
|
||||||
|
QDoubleSpinBox* cellZ_ = nullptr;
|
||||||
|
QDoubleSpinBox* power_ = nullptr;
|
||||||
|
QDoubleSpinBox* maxDist_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -93,6 +93,7 @@
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "SettingsDialog.hpp"
|
#include "SettingsDialog.hpp"
|
||||||
#include "TopBar.hpp"
|
#include "TopBar.hpp"
|
||||||
|
#include "VolumeParamsDialog.hpp"
|
||||||
#include "ProjectListDialog.hpp"
|
#include "ProjectListDialog.hpp"
|
||||||
#include "ObjectFormDialog.hpp"
|
#include "ObjectFormDialog.hpp"
|
||||||
#include "ImportDatasetDialog.hpp"
|
#include "ImportDatasetDialog.hpp"
|
||||||
|
|
@ -251,7 +252,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 中央渲染编排(VtkSceneController + VtkSceneView,取代旧 rebuildCentral lambda 与裸 show* 标志)。
|
// 中央渲染编排(VtkSceneController + VtkSceneView,取代旧 rebuildCentral lambda 与裸 show* 标志)。
|
||||||
// 3D 场景仓储用 Api3dRepository(真实后端:loadSection 走真实 ERT 反演端点,委托 datasetRepo)。
|
// 3D 场景仓储用 Api3dRepository(真实后端:loadSection 走真实 ERT 反演端点,委托 datasetRepo)。
|
||||||
// 视图(VtkSceneView)非 QObject、控制器/3D 仓储亦然:随 scene 一并在 vtkWidget 销毁时清理。
|
// 视图(VtkSceneView)非 QObject、控制器/3D 仓储亦然:随 scene 一并在 vtkWidget 销毁时清理。
|
||||||
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo);
|
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo, frame);
|
||||||
auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr,
|
auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr,
|
||||||
frame, refElev);
|
frame, refElev);
|
||||||
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
||||||
|
|
@ -361,6 +362,27 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
|
|
||||||
// ── 三栏抽屉信号 → 控制器/交互(Task 7 接线)──────────────────────────────
|
// ── 三栏抽屉信号 → 控制器/交互(Task 7 接线)──────────────────────────────
|
||||||
auto* c3 = drawer->col3D();
|
auto* c3 = drawer->col3D();
|
||||||
|
|
||||||
|
// 三维分析栏 = 后端 Analysis 行(dd_slice) + 客户端创建的三维体(mock)。生成的三维体是"分析产物"
|
||||||
|
// (设计 §2.1:三维分析栏按 对象/三维体模型/切片 三级树),归三维分析栏(非数据集栏)。
|
||||||
|
// 后端列表每次勾选测线全量覆盖,故客户端体单独维护、刷新时合并注入(不被冲掉)。
|
||||||
|
auto lastAnalysisRows = std::make_shared<std::vector<geopro::data::DsRow>>();
|
||||||
|
auto refreshAnalysis = [drawer, scene3dRepo, lastAnalysisRows]() {
|
||||||
|
std::vector<geopro::data::DsRow> rows = *lastAnalysisRows;
|
||||||
|
for (auto& vr : scene3dRepo->volumeRows()) rows.push_back(std::move(vr));
|
||||||
|
drawer->colAnalysis()->setDatasets(rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染勾选聚合:三维数据集栏(剖面→帘面)+ 三维分析栏(三维体/切片→体素/切片)两套勾选并集
|
||||||
|
// 后下发控制器(setCheckedDatasets 全量 diff,须并集;否则一栏勾选会清掉另一栏的图元)。
|
||||||
|
auto checkedProfiles = std::make_shared<QStringList>();
|
||||||
|
auto checkedAnalysis = std::make_shared<QStringList>();
|
||||||
|
auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis]() {
|
||||||
|
QStringList all = *checkedProfiles;
|
||||||
|
all += *checkedAnalysis;
|
||||||
|
sceneCtrl->setCheckedDatasets(all);
|
||||||
|
};
|
||||||
|
|
||||||
QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl,
|
QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl,
|
||||||
&geopro::controller::VtkSceneController::setAxesMode);
|
&geopro::controller::VtkSceneController::setAxesMode);
|
||||||
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
|
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
|
||||||
|
|
@ -375,17 +397,39 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
&geopro::controller::VtkSceneController::zoomOut);
|
&geopro::controller::VtkSceneController::zoomOut);
|
||||||
QObject::connect(c3, &geopro::app::Column3DDataset::fitRequested, sceneCtrl,
|
QObject::connect(c3, &geopro::app::Column3DDataset::fitRequested, sceneCtrl,
|
||||||
&geopro::controller::VtkSceneController::fit);
|
&geopro::controller::VtkSceneController::fit);
|
||||||
// 渲染勾选的 3D 数据集:真实 ds id 直达控制器异步帘面路径
|
// 三维数据集栏勾选(反演剖面)→ 并入渲染勾选集(剖面走帘面路径)。
|
||||||
// (setCheckedDatasets → Api3dRepository.loadSection(realId) → 真实 ERT 反演端点 → 真实帘面)。
|
|
||||||
QObject::connect(c3, &geopro::app::Column3DDataset::checkedDatasetsChanged, sceneCtrl,
|
QObject::connect(c3, &geopro::app::Column3DDataset::checkedDatasetsChanged, sceneCtrl,
|
||||||
&geopro::controller::VtkSceneController::setCheckedDatasets);
|
[checkedProfiles, pushChecked](const QStringList& ids) {
|
||||||
|
*checkedProfiles = ids;
|
||||||
|
pushChecked();
|
||||||
|
});
|
||||||
// O点位置/字体本期 stub(TODO P4:弹框)。
|
// O点位置/字体本期 stub(TODO P4:弹框)。
|
||||||
QObject::connect(c3, &geopro::app::Column3DDataset::oPointClicked, vtkWidget,
|
QObject::connect(c3, &geopro::app::Column3DDataset::oPointClicked, vtkWidget,
|
||||||
[]() { /* TODO P4: O点位置弹框 */ });
|
[]() { /* TODO P4: O点位置弹框 */ });
|
||||||
QObject::connect(c3, &geopro::app::Column3DDataset::fontClicked, vtkWidget,
|
QObject::connect(c3, &geopro::app::Column3DDataset::fontClicked, vtkWidget,
|
||||||
[]() { /* TODO P4: 字体弹框 */ });
|
[]() { /* TODO P4: 字体弹框 */ });
|
||||||
|
// 三维数据集栏右键「生成三维体」:弹参数对话框 → 客户端 createVolume(mock)→ 刷新三维分析栏
|
||||||
|
// (新三维体作为"分析产物"出现在三维分析栏,勾选即渲染体)。
|
||||||
|
QObject::connect(c3, &geopro::app::Column3DDataset::generateVolumeRequested, &window,
|
||||||
|
[&window, scene3dRepo, refreshAnalysis](const QStringList& sourceIds) {
|
||||||
|
geopro::app::VolumeParamsDialog dlg(static_cast<int>(sourceIds.size()),
|
||||||
|
&window);
|
||||||
|
if (dlg.exec() != QDialog::Accepted) return;
|
||||||
|
geopro::data::VolumeBuildParams params = dlg.params();
|
||||||
|
for (const QString& id : sourceIds)
|
||||||
|
params.sourceDatasetIds.push_back(id.toStdString());
|
||||||
|
scene3dRepo->createVolume(std::move(params),
|
||||||
|
dlg.volumeName().toStdString());
|
||||||
|
refreshAnalysis(); // 新体行进入三维分析栏,勾选即渲染体
|
||||||
|
});
|
||||||
|
|
||||||
auto* ca = drawer->colAnalysis();
|
auto* ca = drawer->colAnalysis();
|
||||||
|
// 三维分析栏勾选(三维体/切片)→ 并入渲染勾选集(体走体素路径,由 isVolumeDataset 分流)。
|
||||||
|
QObject::connect(ca, &geopro::app::Column3DAnalysis::checkedItemsChanged, sceneCtrl,
|
||||||
|
[checkedAnalysis, pushChecked](const QStringList& ids) {
|
||||||
|
*checkedAnalysis = ids;
|
||||||
|
pushChecked();
|
||||||
|
});
|
||||||
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceRequested, vtkWidget,
|
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceRequested, vtkWidget,
|
||||||
[interactionMgr](geopro::render::interact::SliceAxis axis) {
|
[interactionMgr](geopro::render::interact::SliceAxis axis) {
|
||||||
interactionMgr->addSlice(axis);
|
interactionMgr->addSlice(axis);
|
||||||
|
|
@ -629,24 +673,27 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto generation = std::make_shared<unsigned long long>(0);
|
auto generation = std::make_shared<unsigned long long>(0);
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &window,
|
objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &window,
|
||||||
[&projectRepo, &nav, drawer, emptyState, generation](const QStringList& tmIds) {
|
[&projectRepo, &nav, drawer, emptyState, generation, lastAnalysisRows,
|
||||||
|
refreshAnalysis](const QStringList& tmIds) {
|
||||||
const unsigned long long myGen = ++(*generation);
|
const unsigned long long myGen = ++(*generation);
|
||||||
emptyState->setVisible(tmIds.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染
|
emptyState->setVisible(tmIds.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染
|
||||||
if (tmIds.isEmpty()) {
|
if (tmIds.isEmpty()) {
|
||||||
drawer->col3D()->setDatasets({});
|
drawer->col3D()->setDatasets({});
|
||||||
drawer->col2D()->setDatasets({});
|
drawer->col2D()->setDatasets({});
|
||||||
drawer->colAnalysis()->setDatasets({});
|
*lastAnalysisRows = {};
|
||||||
|
refreshAnalysis(); // 后端分析行清空,但客户端三维体仍驻留三维分析栏
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 多 TM 异步汇总:每个 TM 取整棵 ds 子树,全部回来后按维度分发到三栏。
|
// 多 TM 异步汇总:每个 TM 取整棵 ds 子树,全部回来后按维度分发到三栏。
|
||||||
auto acc = std::make_shared<std::vector<geopro::data::DsRow>>();
|
auto acc = std::make_shared<std::vector<geopro::data::DsRow>>();
|
||||||
auto remaining = std::make_shared<int>(tmIds.size());
|
auto remaining = std::make_shared<int>(tmIds.size());
|
||||||
auto finish = [acc, drawer, generation, myGen]() {
|
auto finish = [acc, drawer, generation, myGen, lastAnalysisRows, refreshAnalysis]() {
|
||||||
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
|
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
|
||||||
geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc);
|
geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc);
|
||||||
drawer->col3D()->setDatasets(b.dim3D);
|
drawer->col3D()->setDatasets(b.dim3D);
|
||||||
drawer->col2D()->setDatasets(b.dim2D);
|
drawer->col2D()->setDatasets(b.dim2D);
|
||||||
drawer->colAnalysis()->setDatasets(b.analysis);
|
*lastAnalysisRows = b.analysis;
|
||||||
|
refreshAnalysis(); // 后端切片 + 客户端三维体合并注入三维分析栏
|
||||||
};
|
};
|
||||||
for (const QString& tm : tmIds) {
|
for (const QString& tm : tmIds) {
|
||||||
geopro::data::NavRequest* req = projectRepo.loadRowsAsync(
|
geopro::data::NavRequest* req = projectRepo.loadRowsAsync(
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,16 @@
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QAction>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QPoint>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QSet>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
|
|
@ -111,10 +116,12 @@ Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) {
|
||||||
root->addLayout(row);
|
root->addLayout(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据集列表(可勾选)
|
// 数据集列表(可勾选 = 渲染选择;多选高亮 + 右键 = 生成三维体的源选择,两者独立)
|
||||||
list_ = new QTreeWidget();
|
list_ = new QTreeWidget();
|
||||||
list_->setHeaderHidden(true);
|
list_->setHeaderHidden(true);
|
||||||
list_->setRootIsDecorated(true);
|
list_->setRootIsDecorated(true);
|
||||||
|
list_->setSelectionMode(QAbstractItemView::ExtendedSelection); // Ctrl/Shift 多选源剖面
|
||||||
|
list_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
applyDatasetCardDelegate(list_);
|
applyDatasetCardDelegate(list_);
|
||||||
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
||||||
QStringList ids;
|
QStringList ids;
|
||||||
|
|
@ -124,9 +131,30 @@ Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) {
|
||||||
}
|
}
|
||||||
emit checkedDatasetsChanged(ids);
|
emit checkedDatasetsChanged(ids);
|
||||||
});
|
});
|
||||||
|
connect(list_, &QTreeWidget::customContextMenuRequested, this,
|
||||||
|
&Column3DDataset::showListContextMenu);
|
||||||
root->addWidget(list_, 1);
|
root->addWidget(list_, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Column3DDataset::showListContextMenu(const QPoint& pos) {
|
||||||
|
// 收集选中项中"可作三维体源"的数据集(反演剖面类)。
|
||||||
|
static const QSet<QString> kSourceDdCodes = {QStringLiteral("dd_section"),
|
||||||
|
QStringLiteral("dd_inversion_data")};
|
||||||
|
QStringList sourceIds;
|
||||||
|
for (QTreeWidgetItem* item : list_->selectedItems()) {
|
||||||
|
const QString ddCode = item->data(0, kDsDdCodeRole).toString();
|
||||||
|
if (kSourceDdCodes.contains(ddCode))
|
||||||
|
sourceIds << item->data(0, kDsIdRole).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu menu(this);
|
||||||
|
QAction* gen = menu.addAction(QStringLiteral("生成三维体"));
|
||||||
|
gen->setEnabled(!sourceIds.isEmpty()); // 无可作源选中 → 灰显(提示需选反演剖面)
|
||||||
|
QAction* chosen = menu.exec(list_->viewport()->mapToGlobal(pos));
|
||||||
|
if (chosen == gen && !sourceIds.isEmpty())
|
||||||
|
emit generateVolumeRequested(sourceIds);
|
||||||
|
}
|
||||||
|
|
||||||
void Column3DDataset::setVerticalExaggeration(double ve) {
|
void Column3DDataset::setVerticalExaggeration(double ve) {
|
||||||
const int v = std::max(1, static_cast<int>(ve + 0.5));
|
const int v = std::max(1, static_cast<int>(ve + 0.5));
|
||||||
QSignalBlocker block(veSlider_); // 仅同步 UI 显示;传播由组合根分发,避免重复发信号
|
QSignalBlocker block(veSlider_); // 仅同步 UI 显示;传播由组合根分发,避免重复发信号
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,13 @@ signals:
|
||||||
void oPointClicked();
|
void oPointClicked();
|
||||||
void fontClicked();
|
void fontClicked();
|
||||||
void checkedDatasetsChanged(const QStringList& dsIds);
|
void checkedDatasetsChanged(const QStringList& dsIds);
|
||||||
|
// 右键「生成三维体」:选中的源数据集 id(≥1,均为可作源类型)→ 组合根弹参数对话框 + 客户端插值。
|
||||||
|
void generateVolumeRequested(const QStringList& sourceDsIds);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// 右键菜单:选中可作源数据集(dd_section / dd_inversion_data)时提供「生成三维体」。
|
||||||
|
void showListContextMenu(const QPoint& pos);
|
||||||
|
|
||||||
QTreeWidget* list_ = nullptr;
|
QTreeWidget* list_ = nullptr;
|
||||||
QSlider* veSlider_ = nullptr; // 水平/垂直比例滑块
|
QSlider* veSlider_ = nullptr; // 水平/垂直比例滑块
|
||||||
QLabel* veLabel_ = nullptr;
|
QLabel* veLabel_ = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,27 @@ void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
|
||||||
void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long long gen) {
|
void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long long gen) {
|
||||||
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
|
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
|
||||||
QPointer<VtkSceneController> self(this);
|
QPointer<VtkSceneController> self(this);
|
||||||
if (showCurtain_) {
|
|
||||||
|
// 按数据集类型分流(取代旧全局 showCurtain_/showVoxel_ 开关):
|
||||||
|
// 三维体(dd_voxel,客户端创建)→ 体素渲染;其余剖面(dd_section 等)→ 帘面渲染。
|
||||||
|
if (sceneRepo_.isVolumeDataset(dsId)) {
|
||||||
|
auto cachedGrid = volumeCache_.find(dsId);
|
||||||
|
auto cachedScale = volumeScaleCache_.find(dsId);
|
||||||
|
if (cachedGrid != volumeCache_.end() && cachedScale != volumeScaleCache_.end()) {
|
||||||
|
view_.addVolume(dsId, cachedGrid->second, cachedScale->second); // 缓存命中(色阶随体缓存)
|
||||||
|
onDatasetArrived();
|
||||||
|
return;
|
||||||
|
}
|
||||||
loadingDs_.insert(dsId);
|
loadingDs_.insert(dsId);
|
||||||
sceneRepo_.loadSection(
|
sceneRepo_.loadVolume(
|
||||||
dsId,
|
dsId,
|
||||||
[self, gen, dsId](data::SectionData s) {
|
[self, gen, dsId](data::VolumeGrid g, core::ColorScale cs) {
|
||||||
if (!self) return;
|
if (!self) return;
|
||||||
self->loadingDs_.erase(dsId);
|
self->loadingDs_.erase(dsId);
|
||||||
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return; // 作废/已取消
|
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return;
|
||||||
self->view_.addCurtain(dsId, s.grid, s.scale);
|
self->volumeScaleCache_[dsId] = cs; // 色阶随体一起缓存(mock 体在 dsRepo_ 无条目)
|
||||||
|
auto it = self->volumeCache_.emplace(dsId, std::move(g)).first;
|
||||||
|
self->view_.addVolume(dsId, it->second, self->volumeScaleCache_[dsId]);
|
||||||
self->onDatasetArrived();
|
self->onDatasetArrived();
|
||||||
},
|
},
|
||||||
[self, gen, dsId](const std::string& m) {
|
[self, gen, dsId](const std::string& m) {
|
||||||
|
|
@ -66,27 +78,26 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
|
||||||
if (gen != self->rebuildGeneration_) return;
|
if (gen != self->rebuildGeneration_) return;
|
||||||
emit self->loadFailed(QString::fromStdString(m));
|
emit self->loadFailed(QString::fromStdString(m));
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (showVoxel_) {
|
|
||||||
auto cached = volumeCache_.find(dsId);
|
// 剖面 → 帘面(着色用 loadSection 返回的 s.scale,与体的源色阶同源)。
|
||||||
if (cached != volumeCache_.end()) {
|
loadingDs_.insert(dsId);
|
||||||
view_.addVolume(dsId, cached->second, colorScale(dsId));
|
sceneRepo_.loadSection(
|
||||||
onDatasetArrived();
|
dsId,
|
||||||
} else {
|
[self, gen, dsId](data::SectionData s) {
|
||||||
sceneRepo_.loadVolume(
|
if (!self) return;
|
||||||
dsId,
|
self->loadingDs_.erase(dsId);
|
||||||
[self, gen, dsId](data::VolumeGrid g) {
|
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return; // 作废/已取消
|
||||||
if (!self || gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return;
|
self->view_.addCurtain(dsId, s.grid, s.scale);
|
||||||
auto it = self->volumeCache_.emplace(dsId, std::move(g)).first;
|
self->onDatasetArrived();
|
||||||
self->view_.addVolume(dsId, it->second, self->colorScale(dsId));
|
},
|
||||||
self->onDatasetArrived();
|
[self, gen, dsId](const std::string& m) {
|
||||||
},
|
if (!self) return;
|
||||||
[self, gen](const std::string& m) {
|
self->loadingDs_.erase(dsId);
|
||||||
if (!self || gen != self->rebuildGeneration_) return;
|
if (gen != self->rebuildGeneration_) return;
|
||||||
emit self->loadFailed(QString::fromStdString(m));
|
emit self->loadFailed(QString::fromStdString(m));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneController::onDatasetArrived() {
|
void VtkSceneController::onDatasetArrived() {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@ private:
|
||||||
std::map<std::string, geopro::core::Grid> gridCache_;
|
std::map<std::string, geopro::core::Grid> gridCache_;
|
||||||
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
|
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
|
||||||
std::map<std::string, data::VolumeGrid> volumeCache_;
|
std::map<std::string, data::VolumeGrid> volumeCache_;
|
||||||
|
// 三维体色阶缓存:mock 体在 dsRepo_ 无条目,色阶随 loadVolume 一起交付并缓存于此。
|
||||||
|
std::map<std::string, geopro::core::ColorScale> volumeScaleCache_;
|
||||||
|
|
||||||
// 异步回灌防护:每次全量 rebuild 自增,回调比对丢弃迟到结果。
|
// 异步回灌防护:每次全量 rebuild 自增,回调比对丢弃迟到结果。
|
||||||
unsigned long long rebuildGeneration_ = 0;
|
unsigned long long rebuildGeneration_ = 0;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ add_library(geopro_core STATIC
|
||||||
geo/CrsTransform.cpp
|
geo/CrsTransform.cpp
|
||||||
model/ColorScale.cpp
|
model/ColorScale.cpp
|
||||||
algo/IdwInterpolator.cpp
|
algo/IdwInterpolator.cpp
|
||||||
|
algo/VolumeBuilder.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
#include "algo/VolumeBuilder.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "algo/IdwInterpolator.hpp"
|
||||||
|
|
||||||
|
namespace geopro::core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// ext(包络长度)/ cell(间距)→ 网格点数,限幅 [1, kMaxVolumeDim]。
|
||||||
|
int clampDim(double ext, double cell) {
|
||||||
|
int n = static_cast<int>(ext / cell) + 1;
|
||||||
|
if (n < 1) n = 1;
|
||||||
|
if (n > kMaxVolumeDim) n = kMaxVolumeDim;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BuiltVolume buildVolume(const PointSet& pts, double cellXY, double cellZ,
|
||||||
|
double power, double maxDist) {
|
||||||
|
if (pts.v.empty()) {
|
||||||
|
throw std::invalid_argument("buildVolume: empty point set");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 点集包络。
|
||||||
|
double minx = pts.x[0], maxx = pts.x[0];
|
||||||
|
double miny = pts.y[0], maxy = pts.y[0];
|
||||||
|
double minz = pts.z[0], maxz = pts.z[0];
|
||||||
|
for (std::size_t i = 1; i < pts.v.size(); ++i) {
|
||||||
|
minx = std::min(minx, pts.x[i]); maxx = std::max(maxx, pts.x[i]);
|
||||||
|
miny = std::min(miny, pts.y[i]); maxy = std::max(maxy, pts.y[i]);
|
||||||
|
minz = std::min(minz, pts.z[i]); maxz = std::max(maxz, pts.z[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) GridSpec(角点对齐 = 原点取包络最小角)。
|
||||||
|
GridSpec spec{};
|
||||||
|
spec.ox = minx; spec.oy = miny; spec.oz = minz;
|
||||||
|
spec.dx = cellXY; spec.dy = cellXY; spec.dz = cellZ;
|
||||||
|
spec.nx = clampDim(maxx - minx, cellXY);
|
||||||
|
spec.ny = clampDim(maxy - miny, cellXY);
|
||||||
|
spec.nz = clampDim(maxz - minz, cellZ);
|
||||||
|
spec.power = power;
|
||||||
|
spec.maxDist = maxDist;
|
||||||
|
|
||||||
|
// 3) IDW(maxDist 外 NaN 留空)。
|
||||||
|
const IdwInterpolator idw;
|
||||||
|
ScalarVolume vol = idw.interpolate(pts, spec);
|
||||||
|
|
||||||
|
// 4) 数据实测值域(仅有限值)。无有限值 → 退化 {0,1}。
|
||||||
|
double vmin = std::numeric_limits<double>::infinity();
|
||||||
|
double vmax = -std::numeric_limits<double>::infinity();
|
||||||
|
for (double v : vol.data()) {
|
||||||
|
if (std::isnan(v)) continue;
|
||||||
|
vmin = std::min(vmin, v); vmax = std::max(vmax, v);
|
||||||
|
}
|
||||||
|
if (!(vmin < vmax)) { vmin = 0.0; vmax = 1.0; }
|
||||||
|
|
||||||
|
return BuiltVolume{std::move(vol), spec, vmin, vmax};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::core
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
#include "algo/IInterpolator.hpp"
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
|
||||||
|
namespace geopro::core {
|
||||||
|
|
||||||
|
// 网格维度上限(与原 LocalSample3dRepository kMaxDim 同口径,防超大体素爆内存)。
|
||||||
|
constexpr int kMaxVolumeDim = 400;
|
||||||
|
|
||||||
|
// buildVolume 产物:插值体 + 网格规格 + 数据实测值域。
|
||||||
|
// ScalarVolume 无默认构造 ⇒ BuiltVolume 亦无默认构造,须聚合初始化全部成员。
|
||||||
|
struct BuiltVolume {
|
||||||
|
ScalarVolume vol;
|
||||||
|
GridSpec spec;
|
||||||
|
double vmin, vmax; // 数据实测有限值范围(无有限值时退化为 {0,1})
|
||||||
|
};
|
||||||
|
|
||||||
|
// 散点(世界局部米)→ 包络盒角点对齐 GridSpec(维度按 ext/cell 限幅到 [1, kMaxVolumeDim])
|
||||||
|
// → IDW → ScalarVolume(maxDist 外 NaN 留空)→ 数据实测 vmin/vmax。
|
||||||
|
// 前置:pts 须含 ≥1 点(空集抛 std::invalid_argument)。
|
||||||
|
// 确定性:同点集同参数 → 同 GridSpec 同体(不冻结 spec,见计划 §1 决策)。
|
||||||
|
// 提取自 LocalSample3dRepository::loadVolume,供本地样本 / 真实 Api 共享,消除调参漂移。
|
||||||
|
BuiltVolume buildVolume(const PointSet& pts, double cellXY, double cellZ,
|
||||||
|
double power, double maxDist);
|
||||||
|
|
||||||
|
} // namespace geopro::core
|
||||||
|
|
@ -3,9 +3,16 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <exception>
|
||||||
|
#include <memory>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "algo/VolumeBuilder.hpp" // core::PointSet / BuiltVolume / buildVolume(含 Field.hpp)
|
||||||
#include "api/DatasetLoadHandles.hpp"
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/detail/DetailPayloads.hpp"
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
#include "repo/IAsyncDatasetRepository.hpp"
|
#include "repo/IAsyncDatasetRepository.hpp"
|
||||||
|
|
||||||
|
|
@ -15,7 +22,9 @@ namespace {
|
||||||
constexpr const char* kNotReady = "后端三维端点未就绪";
|
constexpr const char* kNotReady = "后端三维端点未就绪";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Api3dRepository::Api3dRepository(IAsyncDatasetRepository& dsRepo) : dsRepo_(dsRepo) {}
|
Api3dRepository::Api3dRepository(IAsyncDatasetRepository& dsRepo,
|
||||||
|
std::shared_ptr<core::GeoLocalFrame> frame)
|
||||||
|
: dsRepo_(dsRepo), frame_(std::move(frame)) {}
|
||||||
|
|
||||||
DsDimension Api3dRepository::dimensionOf(const DsRow& ds) const {
|
DsDimension Api3dRepository::dimensionOf(const DsRow& ds) const {
|
||||||
// 与 LocalSample3dRepository::dimensionOf 同口径(spec §6.1 ddCode→维度)。
|
// 与 LocalSample3dRepository::dimensionOf 同口径(spec §6.1 ddCode→维度)。
|
||||||
|
|
@ -54,9 +63,141 @@ void Api3dRepository::loadSection(const std::string& dsId, std::function<void(Se
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Api3dRepository::loadVolume(const std::string& /*dsId*/,
|
bool Api3dRepository::isVolumeDataset(const std::string& dsId) const {
|
||||||
std::function<void(VolumeGrid)> /*onOk*/, OnError onErr) {
|
return volumes_.find(dsId) != volumes_.end();
|
||||||
onErr(kNotReady); // 后端三维体端点未就绪
|
}
|
||||||
|
|
||||||
|
std::string Api3dRepository::createVolume(VolumeBuildParams params, const std::string& name) {
|
||||||
|
const std::string id = "vol-" + std::to_string(++volumeCounter_);
|
||||||
|
volumes_[id] = StoredVolume{std::move(params), name, std::nullopt};
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DsRow> Api3dRepository::volumeRows() const {
|
||||||
|
std::vector<DsRow> rows;
|
||||||
|
rows.reserve(volumes_.size());
|
||||||
|
for (const auto& [id, sv] : volumes_) {
|
||||||
|
DsRow r;
|
||||||
|
r.id = id;
|
||||||
|
r.dsName = sv.name;
|
||||||
|
r.ddCode = "dd_voxel";
|
||||||
|
r.typeName = "三维体";
|
||||||
|
rows.push_back(std::move(r));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Api3dRepository::appendGridPoints(const core::Grid& g, core::PointSet& pts) const {
|
||||||
|
const int nx = g.nx(), ny = g.ny();
|
||||||
|
if (nx < 1 || ny < 1 || g.y.size() < static_cast<std::size_t>(ny)) return;
|
||||||
|
// 与 CurtainActor::buildCurtain 同口径:有 lat/lon 用 frame.toLocal,否则退化用 g.x/0。
|
||||||
|
const bool hasLatLon = g.lat.size() >= static_cast<std::size_t>(nx) &&
|
||||||
|
g.lon.size() >= static_cast<std::size_t>(nx);
|
||||||
|
for (int j = 0; j < ny; ++j) {
|
||||||
|
for (int i = 0; i < nx; ++i) {
|
||||||
|
const double val = g.valueAt(i, j);
|
||||||
|
if (!std::isfinite(val)) continue; // 跳过无数据格(与帘面消隐一致,避免 NaN 入 IDW)
|
||||||
|
double px, py;
|
||||||
|
if (hasLatLon) {
|
||||||
|
const auto p = frame_->toLocal(g.lat[i], g.lon[i]);
|
||||||
|
px = p.x;
|
||||||
|
py = p.y;
|
||||||
|
} else {
|
||||||
|
px = (g.x.size() > static_cast<std::size_t>(i)) ? g.x[i] : static_cast<double>(i);
|
||||||
|
py = 0.0;
|
||||||
|
}
|
||||||
|
pts.x.push_back(px);
|
||||||
|
pts.y.push_back(py);
|
||||||
|
pts.z.push_back(g.y[j]); // 世界 Z = 高程(与 CurtainActor 一致)
|
||||||
|
pts.v.push_back(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Api3dRepository::finalizeVolume(const std::string& dsId, const core::PointSet& pts,
|
||||||
|
const core::ColorScale& scale,
|
||||||
|
const VolumeBuildParams& params,
|
||||||
|
std::function<void(VolumeGrid, core::ColorScale)> onOk,
|
||||||
|
OnError onErr) {
|
||||||
|
if (pts.v.empty()) {
|
||||||
|
onErr("Api3dRepository::loadVolume: 配准后无有效散点(源数据为空或全无数据)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
geopro::core::BuiltVolume bv =
|
||||||
|
geopro::core::buildVolume(pts, params.cellXY, params.cellZ, params.power, params.maxDist);
|
||||||
|
// 值域:优先色阶分段值,否则 buildVolume 的数据实测范围。
|
||||||
|
double vmin = bv.vmin, vmax = bv.vmax;
|
||||||
|
const std::vector<double> stops = scale.stopValues();
|
||||||
|
if (stops.size() >= 2) {
|
||||||
|
vmin = stops.front();
|
||||||
|
vmax = stops.back();
|
||||||
|
}
|
||||||
|
VolumeGrid out{std::move(bv.vol),
|
||||||
|
{{bv.spec.ox, bv.spec.oy, bv.spec.oz}},
|
||||||
|
{{bv.spec.dx, bv.spec.dy, bv.spec.dz}},
|
||||||
|
vmin, vmax};
|
||||||
|
auto it = volumes_.find(dsId);
|
||||||
|
if (it != volumes_.end()) { // 缓存明细 + 色阶(下次命中即跳重算)
|
||||||
|
it->second.cachedGrid = out;
|
||||||
|
it->second.cachedScale = scale;
|
||||||
|
}
|
||||||
|
onOk(std::move(out), scale);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
onErr(std::string("Api3dRepository::loadVolume: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Api3dRepository::loadVolume(const std::string& dsId,
|
||||||
|
std::function<void(VolumeGrid, core::ColorScale)> onOk,
|
||||||
|
OnError onErr) {
|
||||||
|
auto it = volumes_.find(dsId);
|
||||||
|
if (it == volumes_.end()) {
|
||||||
|
onErr("Api3dRepository::loadVolume: 未知三维体 " + dsId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StoredVolume& sv = it->second;
|
||||||
|
if (sv.cachedGrid) { // 明细命中 → 直接渲染(不重算)
|
||||||
|
onOk(*sv.cachedGrid, sv.cachedScale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const VolumeBuildParams params = sv.params; // 拷贝:异步回调期间存储可能变动
|
||||||
|
if (params.sourceDatasetIds.empty()) {
|
||||||
|
onErr("Api3dRepository::loadVolume: 三维体无源数据集");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多源扇出:每个源走 loadSection(与帘面同一 inversion.grid 路径 → 同系对齐),
|
||||||
|
// 主线程聚合(loadSection 回调在主线程)。任一源失败 → 整体失败(只回一次)。
|
||||||
|
struct Agg {
|
||||||
|
int pending;
|
||||||
|
bool failed = false;
|
||||||
|
core::PointSet pts;
|
||||||
|
core::ColorScale scale; // 取首个到达源的色阶定值域
|
||||||
|
bool haveScale = false;
|
||||||
|
};
|
||||||
|
auto agg = std::make_shared<Agg>();
|
||||||
|
agg->pending = static_cast<int>(params.sourceDatasetIds.size());
|
||||||
|
|
||||||
|
for (const std::string& srcId : params.sourceDatasetIds) {
|
||||||
|
loadSection(
|
||||||
|
srcId,
|
||||||
|
[this, dsId, params, agg, onOk, onErr](SectionData s) {
|
||||||
|
if (agg->failed) return;
|
||||||
|
appendGridPoints(s.grid, agg->pts);
|
||||||
|
if (!agg->haveScale) {
|
||||||
|
agg->scale = s.scale;
|
||||||
|
agg->haveScale = true;
|
||||||
|
}
|
||||||
|
if (--agg->pending > 0) return; // 还有源未到齐
|
||||||
|
finalizeVolume(dsId, agg->pts, agg->scale, params, onOk, onErr);
|
||||||
|
},
|
||||||
|
[agg, onErr](const std::string& m) {
|
||||||
|
if (agg->failed) return;
|
||||||
|
agg->failed = true;
|
||||||
|
onErr("Api3dRepository::loadVolume 源加载失败: " + m);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Api3dRepository::loadTerrainPaths(std::function<void(TerrainPaths)> /*onOk*/, OnError onErr) {
|
void Api3dRepository::loadTerrainPaths(std::function<void(TerrainPaths)> /*onOk*/, OnError onErr) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
#include "repo/I3dSceneRepository.hpp"
|
#include "repo/I3dSceneRepository.hpp"
|
||||||
|
#include "repo/VolumeBuildParams.hpp"
|
||||||
|
|
||||||
|
namespace geopro::core {
|
||||||
|
struct PointSet; // algo/IInterpolator.hpp(Grid/ColorScale 经 I3dSceneRepository.hpp 已可见)
|
||||||
|
} // namespace geopro::core
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
|
|
||||||
|
|
@ -19,11 +28,21 @@ class IAsyncDatasetRepository;
|
||||||
// 给用户明确"未实现"而非假成功。
|
// 给用户明确"未实现"而非假成功。
|
||||||
class Api3dRepository : public I3dSceneRepository {
|
class Api3dRepository : public I3dSceneRepository {
|
||||||
public:
|
public:
|
||||||
explicit Api3dRepository(IAsyncDatasetRepository& dsRepo);
|
// frame:全项目共享 GeoLocalFrame(与帘面/底图同一对象)——三维体散点按其 lat/lon→局部米
|
||||||
|
// 配准,保证与帘面构造性对齐(含运行期 reanchor)。
|
||||||
|
Api3dRepository(IAsyncDatasetRepository& dsRepo, std::shared_ptr<core::GeoLocalFrame> frame);
|
||||||
|
|
||||||
DsDimension dimensionOf(const DsRow& ds) const override;
|
DsDimension dimensionOf(const DsRow& ds) const override;
|
||||||
|
bool isVolumeDataset(const std::string& dsId) const override;
|
||||||
|
|
||||||
void loadVolume(const std::string& dsId, std::function<void(VolumeGrid)> onOk,
|
// ── 客户端创建三维体(mock 持久化:内存;端点就绪后换实现)──────────────────
|
||||||
|
// 登记新三维体(仅存参数,不立即插值)→ 返回新 dsId("vol-N")。插值在首次 loadVolume 惰性做并缓存。
|
||||||
|
std::string createVolume(VolumeBuildParams params, const std::string& name);
|
||||||
|
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维数据集栏合并注入(每次 setDatasets 追加)。
|
||||||
|
std::vector<DsRow> volumeRows() const;
|
||||||
|
|
||||||
|
void loadVolume(const std::string& dsId,
|
||||||
|
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,
|
||||||
OnError onErr) override;
|
OnError onErr) override;
|
||||||
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
|
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
|
||||||
OnError onErr) override;
|
OnError onErr) override;
|
||||||
|
|
@ -56,7 +75,26 @@ public:
|
||||||
OnError onErr) override;
|
OnError onErr) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// 把一条剖面 section grid 的有限值节点按 CurtainActor 同口径定位(lat/lon→frame.toLocal,
|
||||||
|
// 世界 Z=grid.y 高程)追加为 IDW 散点 → 保证体与帘面同系对齐。
|
||||||
|
void appendGridPoints(const core::Grid& g, core::PointSet& pts) const;
|
||||||
|
// 多源散点聚合完成 → buildVolume + 值域 + 缓存明细/色阶 → onOk(体, 色阶)。
|
||||||
|
void finalizeVolume(const std::string& dsId, const core::PointSet& pts,
|
||||||
|
const core::ColorScale& scale, const VolumeBuildParams& params,
|
||||||
|
std::function<void(VolumeGrid, core::ColorScale)> onOk, OnError onErr);
|
||||||
|
|
||||||
IAsyncDatasetRepository& dsRepo_;
|
IAsyncDatasetRepository& dsRepo_;
|
||||||
|
std::shared_ptr<core::GeoLocalFrame> frame_;
|
||||||
|
|
||||||
|
// 内存态三维体存储(mock;重启清空)。cachedGrid = 已插值明细(命中即跳过重算)。
|
||||||
|
struct StoredVolume {
|
||||||
|
VolumeBuildParams params;
|
||||||
|
std::string name;
|
||||||
|
std::optional<VolumeGrid> cachedGrid;
|
||||||
|
core::ColorScale cachedScale; // 与 cachedGrid 同时填(源剖面色阶)
|
||||||
|
};
|
||||||
|
std::map<std::string, StoredVolume> volumes_; // dsId → 体
|
||||||
|
int volumeCounter_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::data
|
} // namespace geopro::data
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,16 @@ public:
|
||||||
// 同步纯函数:ds 类型 → 维度(spec §6.1 映射表)。
|
// 同步纯函数:ds 类型 → 维度(spec §6.1 映射表)。
|
||||||
virtual DsDimension dimensionOf(const DsRow& ds) const = 0;
|
virtual DsDimension dimensionOf(const DsRow& ds) const = 0;
|
||||||
|
|
||||||
// 异步:加载三维体模型(成功回调 VolumeGrid,失败回调消息)。
|
// 同步纯函数:该 dsId 是否为三维体数据集 → 控制器据此选「体素」而非「帘面」渲染路径
|
||||||
|
// (取代旧的全局 showVoxel/showCurtain 图层开关,按数据集类型分流)。
|
||||||
|
virtual bool isVolumeDataset(const std::string& dsId) const = 0;
|
||||||
|
|
||||||
|
// 异步:加载三维体模型(成功回调 VolumeGrid + 其色阶,失败回调消息)。
|
||||||
|
// 色阶随体交付(= 源剖面色阶,与帘面一致):体的着色是其加载表示的固有部分,
|
||||||
|
// 不可由 dsId 经普通数据集仓储取(客户端 mock 体在后端无条目)。
|
||||||
virtual void loadVolume(const std::string& dsId,
|
virtual void loadVolume(const std::string& dsId,
|
||||||
std::function<void(VolumeGrid)> onOk, OnError onErr) = 0;
|
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,
|
||||||
|
OnError onErr) = 0;
|
||||||
|
|
||||||
// 异步:加载剖面(帘面)着色数据(Grid+色阶)。本地样本同步回调;Api 实现走 ERT 反演端点异步回调。
|
// 异步:加载剖面(帘面)着色数据(Grid+色阶)。本地样本同步回调;Api 实现走 ERT 反演端点异步回调。
|
||||||
virtual void loadSection(const std::string& dsId,
|
virtual void loadSection(const std::string& dsId,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "algo/IdwInterpolator.hpp"
|
#include "algo/VolumeBuilder.hpp"
|
||||||
#include "geo/CrsTransform.hpp"
|
#include "geo/CrsTransform.hpp"
|
||||||
#include "geo/GeoLocalFrame.hpp"
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
|
|
@ -20,31 +20,20 @@ namespace geopro::data {
|
||||||
using geopro::core::ColorScale;
|
using geopro::core::ColorScale;
|
||||||
using geopro::core::CrsTransform;
|
using geopro::core::CrsTransform;
|
||||||
using geopro::core::GeoLocalFrame;
|
using geopro::core::GeoLocalFrame;
|
||||||
using geopro::core::GridSpec;
|
|
||||||
using geopro::core::IdwInterpolator;
|
|
||||||
using geopro::core::PointSet;
|
using geopro::core::PointSet;
|
||||||
using geopro::core::ScalarVolume;
|
|
||||||
using geopro::core::ScatterField;
|
using geopro::core::ScatterField;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// 与 render::VoxelFromScatters 的默认参数同口径(保持渲染/切片纵向一致)。
|
// 与 render::VoxelFromScatters 的默认参数同口径(保持渲染/切片纵向一致)。
|
||||||
// TODO(P2/P3): 与 render::buildVoxelFromScatters 的 cellXY/cellZ/power/maxDist 默认值重复,
|
// 「散点→GridSpec→IDW→ScalarVolume」已提到 core::buildVolume 共享(与 Api3dRepository 同源,
|
||||||
// 宜把"散点→配准→GridSpec→IDW→ScalarVolume"提到 core::algo 共享,避免单方调参静默不一致。
|
// 消除调参漂移);此处仅保留默认参数 + 本地样本配准。
|
||||||
constexpr double kCellXY = 1.0;
|
constexpr double kCellXY = 1.0;
|
||||||
constexpr double kCellZ = 0.5;
|
constexpr double kCellZ = 0.5;
|
||||||
constexpr double kPower = 2.0;
|
constexpr double kPower = 2.0;
|
||||||
constexpr double kMaxDist = 4.0;
|
constexpr double kMaxDist = 4.0;
|
||||||
constexpr int kMaxDim = 400;
|
|
||||||
constexpr const char* kWgs84 = "EPSG:4326";
|
constexpr const char* kWgs84 = "EPSG:4326";
|
||||||
|
|
||||||
int clampDim(double ext, double cell) {
|
|
||||||
int n = static_cast<int>(ext / cell) + 1;
|
|
||||||
if (n < 1) n = 1;
|
|
||||||
if (n > kMaxDim) n = kMaxDim;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
LocalSample3dRepository::LocalSample3dRepository(LocalSampleRepository& base, std::string projectCrs,
|
LocalSample3dRepository::LocalSample3dRepository(LocalSampleRepository& base, std::string projectCrs,
|
||||||
|
|
@ -65,8 +54,9 @@ DsDimension LocalSample3dRepository::dimensionOf(const DsRow& ds) const {
|
||||||
return DsDimension::Other;
|
return DsDimension::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LocalSample3dRepository::loadVolume(const std::string& /*dsId*/,
|
void LocalSample3dRepository::loadVolume(
|
||||||
std::function<void(VolumeGrid)> onOk, OnError onErr) {
|
const std::string& /*dsId*/,
|
||||||
|
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk, OnError onErr) {
|
||||||
// P1 样本:dsId 暂未使用,固定读同一组交叉剖面散点→体素(真实 Api 实现按 dsId 取)。
|
// P1 样本:dsId 暂未使用,固定读同一组交叉剖面散点→体素(真实 Api 实现按 dsId 取)。
|
||||||
try {
|
try {
|
||||||
// 1) 读两条交叉剖面散点 + 色阶;配准到世界局部米 + 深度,组装 IDW 输入点集。
|
// 1) 读两条交叉剖面散点 + 色阶;配准到世界局部米 + 深度,组装 IDW 输入点集。
|
||||||
|
|
@ -92,55 +82,28 @@ void LocalSample3dRepository::loadVolume(const std::string& /*dsId*/,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 点集包络 → GridSpec(角点对齐)。
|
// 2) 点集 → GridSpec → IDW → 数据实测值域(共享 core::buildVolume)。
|
||||||
double minx = pts.x[0], maxx = pts.x[0];
|
geopro::core::BuiltVolume bv =
|
||||||
double miny = pts.y[0], maxy = pts.y[0];
|
geopro::core::buildVolume(pts, kCellXY, kCellZ, kPower, kMaxDist);
|
||||||
double minz = pts.z[0], maxz = pts.z[0];
|
|
||||||
for (std::size_t i = 1; i < pts.v.size(); ++i) {
|
|
||||||
minx = std::min(minx, pts.x[i]); maxx = std::max(maxx, pts.x[i]);
|
|
||||||
miny = std::min(miny, pts.y[i]); maxy = std::max(maxy, pts.y[i]);
|
|
||||||
minz = std::min(minz, pts.z[i]); maxz = std::max(maxz, pts.z[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
GridSpec spec{};
|
// 3) 值域:优先 colorBar 真实分段值,否则 buildVolume 的数据实测范围。
|
||||||
spec.ox = minx; spec.oy = miny; spec.oz = minz;
|
double vmin = bv.vmin, vmax = bv.vmax;
|
||||||
spec.dx = kCellXY; spec.dy = kCellXY; spec.dz = kCellZ;
|
|
||||||
spec.nx = clampDim(maxx - minx, kCellXY);
|
|
||||||
spec.ny = clampDim(maxy - miny, kCellXY);
|
|
||||||
spec.nz = clampDim(maxz - minz, kCellZ);
|
|
||||||
spec.power = kPower;
|
|
||||||
spec.maxDist = kMaxDist;
|
|
||||||
|
|
||||||
// 3) IDW → ScalarVolume(maxDist 外 NaN 留空)。
|
|
||||||
const IdwInterpolator idw;
|
|
||||||
ScalarVolume vol = idw.interpolate(pts, spec);
|
|
||||||
|
|
||||||
// 4) 值域:优先 colorBar 真实分段值,否则数据实测。
|
|
||||||
double vmin, vmax;
|
|
||||||
ColorScale cs;
|
ColorScale cs;
|
||||||
try {
|
try {
|
||||||
cs = base_.loadScatterColorScale("grid1");
|
cs = base_.loadScatterColorScale("grid1");
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception&) {
|
||||||
// 色阶缺失 → 退化为数据实测范围。
|
// 色阶缺失 → 沿用数据实测范围。
|
||||||
}
|
}
|
||||||
const std::vector<double> stops = cs.stopValues();
|
const std::vector<double> stops = cs.stopValues();
|
||||||
if (stops.size() >= 2) {
|
if (stops.size() >= 2) {
|
||||||
vmin = stops.front(); vmax = stops.back();
|
vmin = stops.front(); vmax = stops.back();
|
||||||
} else {
|
|
||||||
vmin = std::numeric_limits<double>::infinity();
|
|
||||||
vmax = -std::numeric_limits<double>::infinity();
|
|
||||||
for (double v : vol.data()) {
|
|
||||||
if (std::isnan(v)) continue;
|
|
||||||
vmin = std::min(vmin, v); vmax = std::max(vmax, v);
|
|
||||||
}
|
|
||||||
if (!(vmin < vmax)) { vmin = 0.0; vmax = 1.0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VolumeGrid out{std::move(vol),
|
VolumeGrid out{std::move(bv.vol),
|
||||||
{{spec.ox, spec.oy, spec.oz}},
|
{{bv.spec.ox, bv.spec.oy, bv.spec.oz}},
|
||||||
{{spec.dx, spec.dy, spec.dz}},
|
{{bv.spec.dx, bv.spec.dy, bv.spec.dz}},
|
||||||
vmin, vmax};
|
vmin, vmax};
|
||||||
onOk(std::move(out));
|
onOk(std::move(out), std::move(cs));
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
onErr(std::string("LocalSample3dRepository::loadVolume: ") + e.what());
|
onErr(std::string("LocalSample3dRepository::loadVolume: ") + e.what());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ public:
|
||||||
double baseLat, double baseLon);
|
double baseLat, double baseLon);
|
||||||
|
|
||||||
DsDimension dimensionOf(const DsRow& ds) const override;
|
DsDimension dimensionOf(const DsRow& ds) const override;
|
||||||
void loadVolume(const std::string& dsId, std::function<void(VolumeGrid)> onOk,
|
// 本地样本无客户端创建的三维体(样本体经旧 showVoxel 路径,非按 ds 类型分流)→ 恒 false。
|
||||||
|
bool isVolumeDataset(const std::string& /*dsId*/) const override { return false; }
|
||||||
|
void loadVolume(const std::string& dsId,
|
||||||
|
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,
|
||||||
OnError onErr) override;
|
OnError onErr) override;
|
||||||
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
|
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
|
||||||
OnError onErr) override;
|
OnError onErr) override;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
// 三维体构建参数(设计文档 §7.4;用户决策 2026-06-17:不冻结 gridSpec)。
|
||||||
|
// 必存元数据 = 源数据集 + 插值模型/参数 + 色阶来源;它们小且可复现(详情面板展示用)。
|
||||||
|
// gridSpec / values(明细)为派生:每次按源散点确定性重算(IDW 确定 + 源 ds 锁定 →
|
||||||
|
// 结果必然一致),故不存为"冻结锚点",由 Api3dRepository 缓存即可(见计划 §1)。
|
||||||
|
struct VolumeBuildParams {
|
||||||
|
enum class Model { Idw, Kriging }; // 本期仅 Idw 实现;Kriging 为占位(core 暂无)。
|
||||||
|
|
||||||
|
std::vector<std::string> sourceDatasetIds; // 源数据集 id(≥1;被引用即应锁定不可改)
|
||||||
|
Model interpModel = Model::Idw;
|
||||||
|
double cellXY = 1.0; // 水平网格间距(米)
|
||||||
|
double cellZ = 0.5; // 竖向网格间距(米)
|
||||||
|
double power = 2.0; // IDW 幂
|
||||||
|
double maxDist = 4.0; // 超距 blank(约束插值域,外为 NaN)
|
||||||
|
std::string colorScaleId; // 色阶来源 ds(空 = 取首个源的色阶)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
Loading…
Reference in New Issue