Compare commits

..

No commits in common. "b261374cc92c2305fa46ba0ce7cf1cd051625d9e" and "a2e16e18e85b7b5f18338234f5ba2d350ce1353f" have entirely different histories.

21 changed files with 168 additions and 990 deletions

View File

@ -40,11 +40,11 @@
---
## 3. 客户端做法演变
## 3. 客户端当前做法(供确认/纠正)
- **早期(已废弃)**:竖直 Z = `-y[j]`(把 `y` 当"深度向下"),平顶、不随地表。加真实地形底图后暴露问题:剖面整体沉到地下
- **当前2026-06-17**:竖直 Z = `+y[j]`(把 `y` 当**真实高程**),与同样按真实高程渲染的地形底图同系对齐 → 剖面顶≈地表、露出地面(复刻原版观感)。详见 §6
- 仍**未使用 `z``alt/elevation`**
- 竖直坐标用 **`y` 作深度**:每个格子 Z = `-y[j]`(深度向下取负),**未使用 `z``elevation`**
- 这与**二维"数据详情"反演图一致**(详情图 `ContourPlotItem` 也只用 `x`、`y` 画"距离 × 深度"矩形,不用 `z`/`elevation`
- 3D 帘面 = 二维"距离×深度"剖面**立起来 + 沿真实测线lat/lon弯曲****平顶**、不随地表起伏
---
@ -66,17 +66,3 @@
- 现状 (A) 已能正确渲染(与 2D 详情一致),可继续使用。
- 若业务要 (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 推算。

View File

@ -1,87 +1,74 @@
# 交接VTK 三维视图feat/vtk-3d-view
# 交接VTK 三维视图「补充需求」(feat/vtk-3d-view)
> 给下一个会话无缝接手用。更新日期 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` 误纳、已撤回)。
> 给下一个会话无缝接手用。日期 2026-06-16。分支 `feat/vtk-3d-view`HEAD `07f2f25`,工作树干净(仅根目录 grid-*.png/grid-snap.yml 是既有未跟踪文件,非本任务产物,勿动)。
## 1. 背景
- 项目geopro 桌面客户端Qt6 + VTK9 + Qt-ADS dockWindows/MSVC+Ninja`build.bat`
- 任务:实现需求表「补充需求」页 = VTK 三维视图整套结构/交互。需求源:`D:\Projects\GEOPRO\Geopro3.0 需求表.xlsx`「补充需求」
- 原版 web 源码在 **`D:\Git\lanbingtech\commercial-admin`**Vue + three-tile是**复刻的权威参照**threeMap.vue / mapSource.js / src/apis/
- 三栏结构(三维数据集 / 二维数据集 / 三维分析)+ 真实 ERT 反演剖面(帘面)+ 底图地形——这些**已完成**。本会话主要做了**底图/地形 + 剖面垂直配准 + 增量渲染**,并为下一阶段(三维体/切片/异常)做了**设计定稿**
- 项目geopro 桌面客户端Qt6 + VTK9 + ADS dockWindows/MSVC+Ninja。
- 任务:实现需求表「补充需求」页 = **VTK 三维视图整套交互/结构**。需求源:`D:\Projects\GEOPRO\Geopro3.0 需求表.xlsx`「补充需求」(用 openpyxl 读,控制台中文乱码须导 UTF-8 文件再读A1C92)
- 历史:做本需求前已有"基于本地样本数据的原型渲染"(帘面/体素/切片/地形/散点),但在 commit `6241eb3`"CentralScene 数据驱动重构"时**装配代码被摘除**——render 层 actor 完整且有测试,只是没接上。本任务从复活它起步
- 原版 web「数据视图」(`tenant.geomative.cn/#/projectSpace/dataView`) 已 Playwright 实地分析3D = **ThreeTile(Three.js)地球**(非 Cesium)+多瓦片源3D 结果=2D 反演剖面成竖直帘面。**三栏/切片是客户端新需求(web 无三栏)**。详见记忆 [[web-3d-view-threetile]]
## 2. 本会话已完成(均已编译绿 + 提交;用户验收"差不多了"
**底图 + 真实地形**(核心在 `src/app/TileBasemap.{hpp,cpp}`
- 影像=**天地图卫星**(`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点位置」「字体」弹框仍是 stubmain.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同原版可提交
## 2. 关键约束(用户拍板)
- **后端未就绪** → 本轮全部用 `LocalSampleRepository` 静态样本数据驱动;**但仓储接口必须按真实后端形态设计好**`I3dSceneRepository` 异步),将来换 `Api3dRepository` 不动上层。
- **严格按需求,禁止砍功能/改需求**(教训:曾把 F25 砍掉、把双击正视改按钮,被用户纠正)。复刻不确定处必须实地学习(Playwright),禁猜测。
- 全部回复中文。
## 7. 代码地图(关键文件)
- `src/app/TileBasemap.{hpp,cpp}` — 底图+地形(四叉树/剔除/限流/缓存/动态范围/半透明/合并渲染)。
- `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
- `src/render/actors/CurtainActor.cpp` — 帘面Z=+g.y 真实高程)。
- `src/render/interact/{SliceTool,InteractionManager,SlicePlaneMath}.*` — 切片交互。
- `src/render/{VoxelFromScatters,actors/VoxelActor}.*` + `src/core/algo/IdwInterpolator.*` — 体素插值/绘制(三维体复用)。
- `src/data/repo/I3dSceneRepository.hpp` — 接口loadVolume/createSlice/saveSlice/deleteSlice/loadAnomalyTree/saveAnomaly/.../loadTaskRecords
- `src/data/repo/LocalSample3dRepository.cpp`(内存 mock 参考实现)、`src/data/api/Api3dRepository.cpp`(真实路径,多为 stub `kNotReady`,待按设计文档实现)。
- `src/app/panels/columns/{Column3DDataset,Column2DDataset,Column3DAnalysis,ColumnDrawer}.*` — 三栏 UI信号定义全、main.cpp 未全接)。
- `src/app/main.cpp` — 装配/接线(搜 `basemap`/`colAnalysis`/`onCameraChanged`/`kVerticalExaggeration`)。
- `src/data/dto/DatasetChartDto.cpp``parseInversionGrid`x/y/v/lat/lon**未解析 elevation/alt**)。
## 3. 权威文档
- spec`docs/superpowers/specs/2026-06-15-vtk-3d-supplementary-design.md`v2已纳入架构评审 + web 实地分析§4 是补充需求逐行映射表§6 接口设计§14 分期)。
- 计划:`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`。
## 4. 已完成并经用户验收commit 范围 faee28c..07f2f25
- **P1 复活渲染**(`0f521c5`+`53ccdc0`)`VtkSceneController`(编排,异步)+`I3dSceneRepository`/`LocalSample3dRepository`+`I3dSceneView`/`VtkSceneView`+`Scene::addViewProp`(体绘制 vtkVolume 入场)。勾选对象→样本数据→渲染帘面/体素/地形。
- **P2 三维数据集栏**(`3dea339`+样式 `73deb2b`/`86e0772`):坐标轴(标准/三维立体/不显示 vtkCubeAxesActor)、刻度(无/米/英尺/经纬度,GeoLocalFrame::toLatLon)、水平/垂直比例滑块、快捷视图6向、Zoom(In/Out/Fit)。右上工具条浮层(仅三维显示)。
- **P3 切片交互**(`85d4ff5`..`07f2f25`)`src/render/interact/`(SlicePlaneMath/SliceTool/PickInteractorStyle/InteractionManager)。**已验收**
- 上下/前后/左右切片=固定角度可移动(G22-24);任意切片=拖边缘旋转(F25)+拖中间移动。
- 触碰切片→选中+**亮青边框**高亮(未选暗灰)。
- **双击切片→正视**(D40靠 widget StartInteractionEvent 350ms 双击判定)。
- 滚轮→推进**选中**切片(D46);关闭→移除选中;翻转(E55)。
- **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 过滤勾选对象的 dsC9/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 解析 .batUTF-8 中文注释会让解析崩)。
- **Claude 工具跑 build 会间歇被一个 `Start-Process 'C:\Users\corey\...'` 钩子劫持**(环境问题、非项目;只影响 Claude 工具,不影响用户终端)→ 我的构建验证有时静默没跑,会误判"已更新"**交互类改动必须让用户在其终端 `build.bat rebuild` 实测**。
- **Claude 无法 GUI 测试**VTK 交互(切片/旋转/拾取)的正确性只能靠用户实测——纯逻辑(几何/相机数学)抽成单测widget 行为靠目视。
- app 启动需登录(真实 APItenant.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 反复返工,务必先逐条读「补充需求」对应行)。

View File

@ -1,138 +0,0 @@
# 实现计划:客户端「生成三维体」完整流程(#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` |
| 帘面/地形竖向 = +elevationZ=+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-17gridSpec 每次从源散点**确定性重算**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;
```
新增公有方法concretemain.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. UIColumn3DDataset源数据栏多选 + 右键「生成三维体」
> 归属修正2026-06-17用户指出**源选择**在三维数据集栏(剖面池);**生成的体**进**三维分析栏**§7
`Column3DDataset.{hpp,cpp}`(源数据栏):
- 列表 `setSelectionMode(ExtendedSelection)`(多选高亮,独立于 checkbox 渲染勾选)。
- `setContextMenuPolicy(CustomContextMenu)` + 槽:右键弹菜单「生成三维体」,仅当选中项 ≥1 且均为可作源的 ddCodedd_section/dd_inversion_data时启用。
- 新信号 `void generateVolumeRequested(const QStringList& sourceDsIds);`(取选中项的 kDsIdRole
## 6. UI插值参数对话框
新增 `src/app/VolumeParamsDialog.{hpp,cpp}`QDialog与现有对话框同放 app 根目录):
- 名称、插值模型IDW克里金项 disabled 占位、cellXY/cellZ/power/maxDistQDoubleSpinBox默认同 §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.elevationloadSection 路径)。
- **克里金**UI 占位 disabled仅 IDW 实现(设计 §1.1 列了克里金,但 core 仅 IdwInterpolator
- **mock 持久化形态**:本期纯内存(重启丢失),符合设计 §5「次要待确认」本地文件持久化留后续。
- **源 ds 锁定不变式**(替代 gridSpec 冻结,用户决策):被三维体引用的源 ds **不可修改/删除**——保证 IDW 重算确定一致、切片/异常坐标稳定。
- 本期:内存 mock仅留 TODO在源 ds 删除/编辑入口校验"是否被某三维体引用,是则禁止/告警")。
- 推论:不冻结 gridSpec若源真被改 ⇒ 体应失效重建,而非套旧锚点(旧锚点已是脏数据)。

View File

@ -1,176 +0,0 @@
# 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
- **三维体右键**(行 368375切片▸上下/前后/左右/任意)、色阶、显示/隐藏、数据详情。
- **切片右键**(行 376383保存、保存为、导出、删除、色阶、显示/隐藏、数据详情。
### 2.2 切片工具(行 369372, 387389
- 上下/前后/左右切片:在三维体中心或光标位置打开**水平切片工具,角度不可调**。
- 任意切片:初始角度 = 当前视图 45°可任意调整。
- 选中切片滚轮 → 沿法向往内/外推进;切片对象 = 所属三维体(行 389
- 双击切片 → 视角调正为正视该切片(行 386/388
### 2.3 VTK 视图里对切片的右键(行 391399
- **创建异常**:弹异常工具,以光标拾取点为起点圈定;结束保存时弹对话框(截图大小、异常坐标),参考 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 → 3Ddd_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 走数据文件。

View File

@ -64,7 +64,6 @@ add_executable(geopro_desktop WIN32
ImportDatasetDialog.cpp
ExportDatasetDialog.cpp
SettingsDialog.cpp
VolumeParamsDialog.cpp
Logging.cpp
DatasetDimension.cpp
TileBasemap.cpp)

View File

@ -1,87 +0,0 @@
#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

View File

@ -1,33 +0,0 @@
#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

View File

@ -93,7 +93,6 @@
#include "Theme.hpp"
#include "SettingsDialog.hpp"
#include "TopBar.hpp"
#include "VolumeParamsDialog.hpp"
#include "ProjectListDialog.hpp"
#include "ObjectFormDialog.hpp"
#include "ImportDatasetDialog.hpp"
@ -252,7 +251,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 中央渲染编排VtkSceneController + VtkSceneView取代旧 rebuildCentral lambda 与裸 show* 标志)。
// 3D 场景仓储用 Api3dRepository真实后端loadSection 走真实 ERT 反演端点,委托 datasetRepo
// 视图VtkSceneView非 QObject、控制器/3D 仓储亦然:随 scene 一并在 vtkWidget 销毁时清理。
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo, frame);
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo);
auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr,
frame, refElev);
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
@ -362,27 +361,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 三栏抽屉信号 → 控制器/交互Task 7 接线)──────────────────────────────
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,
&geopro::controller::VtkSceneController::setAxesMode);
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
@ -397,39 +375,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
&geopro::controller::VtkSceneController::zoomOut);
QObject::connect(c3, &geopro::app::Column3DDataset::fitRequested, sceneCtrl,
&geopro::controller::VtkSceneController::fit);
// 三维数据集栏勾选(反演剖面)→ 并入渲染勾选集(剖面走帘面路径)。
// 渲染勾选的 3D 数据集:真实 ds id 直达控制器异步帘面路径
// setCheckedDatasets → Api3dRepository.loadSection(realId) → 真实 ERT 反演端点 → 真实帘面)。
QObject::connect(c3, &geopro::app::Column3DDataset::checkedDatasetsChanged, sceneCtrl,
[checkedProfiles, pushChecked](const QStringList& ids) {
*checkedProfiles = ids;
pushChecked();
});
&geopro::controller::VtkSceneController::setCheckedDatasets);
// O点位置/字体本期 stubTODO P4弹框
QObject::connect(c3, &geopro::app::Column3DDataset::oPointClicked, vtkWidget,
[]() { /* TODO P4: O点位置弹框 */ });
QObject::connect(c3, &geopro::app::Column3DDataset::fontClicked, vtkWidget,
[]() { /* TODO P4: 字体弹框 */ });
// 三维数据集栏右键「生成三维体」:弹参数对话框 → 客户端 createVolumemock→ 刷新三维分析栏
// (新三维体作为"分析产物"出现在三维分析栏,勾选即渲染体)。
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();
// 三维分析栏勾选(三维体/切片)→ 并入渲染勾选集(体走体素路径,由 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,
[interactionMgr](geopro::render::interact::SliceAxis axis) {
interactionMgr->addSlice(axis);
@ -673,27 +629,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto generation = std::make_shared<unsigned long long>(0);
QObject::connect(
objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &window,
[&projectRepo, &nav, drawer, emptyState, generation, lastAnalysisRows,
refreshAnalysis](const QStringList& tmIds) {
[&projectRepo, &nav, drawer, emptyState, generation](const QStringList& tmIds) {
const unsigned long long myGen = ++(*generation);
emptyState->setVisible(tmIds.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染
if (tmIds.isEmpty()) {
drawer->col3D()->setDatasets({});
drawer->col2D()->setDatasets({});
*lastAnalysisRows = {};
refreshAnalysis(); // 后端分析行清空,但客户端三维体仍驻留三维分析栏
drawer->colAnalysis()->setDatasets({});
return;
}
// 多 TM 异步汇总:每个 TM 取整棵 ds 子树,全部回来后按维度分发到三栏。
auto acc = std::make_shared<std::vector<geopro::data::DsRow>>();
auto remaining = std::make_shared<int>(tmIds.size());
auto finish = [acc, drawer, generation, myGen, lastAnalysisRows, refreshAnalysis]() {
auto finish = [acc, drawer, generation, myGen]() {
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc);
drawer->col3D()->setDatasets(b.dim3D);
drawer->col2D()->setDatasets(b.dim2D);
*lastAnalysisRows = b.analysis;
refreshAnalysis(); // 后端切片 + 客户端三维体合并注入三维分析栏
drawer->colAnalysis()->setDatasets(b.analysis);
};
for (const QString& tm : tmIds) {
geopro::data::NavRequest* req = projectRepo.loadRowsAsync(

View File

@ -2,16 +2,11 @@
#include <algorithm>
#include <QAbstractItemView>
#include <QAction>
#include <QComboBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QPoint>
#include <QPushButton>
#include <QSet>
#include <QSignalBlocker>
#include <QSlider>
#include <QTreeWidget>
@ -116,12 +111,10 @@ Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) {
root->addLayout(row);
}
// 数据集列表(可勾选 = 渲染选择;多选高亮 + 右键 = 生成三维体的源选择,两者独立
// 数据集列表(可勾选
list_ = new QTreeWidget();
list_->setHeaderHidden(true);
list_->setRootIsDecorated(true);
list_->setSelectionMode(QAbstractItemView::ExtendedSelection); // Ctrl/Shift 多选源剖面
list_->setContextMenuPolicy(Qt::CustomContextMenu);
applyDatasetCardDelegate(list_);
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
QStringList ids;
@ -131,30 +124,9 @@ Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) {
}
emit checkedDatasetsChanged(ids);
});
connect(list_, &QTreeWidget::customContextMenuRequested, this,
&Column3DDataset::showListContextMenu);
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) {
const int v = std::max(1, static_cast<int>(ve + 0.5));
QSignalBlocker block(veSlider_); // 仅同步 UI 显示;传播由组合根分发,避免重复发信号

View File

@ -30,13 +30,8 @@ signals:
void oPointClicked();
void fontClicked();
void checkedDatasetsChanged(const QStringList& dsIds);
// 右键「生成三维体」:选中的源数据集 id≥1均为可作源类型→ 组合根弹参数对话框 + 客户端插值。
void generateVolumeRequested(const QStringList& sourceDsIds);
private:
// 右键菜单选中可作源数据集dd_section / dd_inversion_data时提供「生成三维体」。
void showListContextMenu(const QPoint& pos);
QTreeWidget* list_ = nullptr;
QSlider* veSlider_ = nullptr; // 水平/垂直比例滑块
QLabel* veLabel_ = nullptr;

View File

@ -49,27 +49,15 @@ void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long long gen) {
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
QPointer<VtkSceneController> self(this);
// 按数据集类型分流(取代旧全局 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;
}
if (showCurtain_) {
loadingDs_.insert(dsId);
sceneRepo_.loadVolume(
sceneRepo_.loadSection(
dsId,
[self, gen, dsId](data::VolumeGrid g, core::ColorScale cs) {
[self, gen, dsId](data::SectionData s) {
if (!self) return;
self->loadingDs_.erase(dsId);
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return;
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]);
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return; // 作废/已取消
self->view_.addCurtain(dsId, s.grid, s.scale);
self->onDatasetArrived();
},
[self, gen, dsId](const std::string& m) {
@ -78,26 +66,27 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
if (gen != self->rebuildGeneration_) return;
emit self->loadFailed(QString::fromStdString(m));
});
return;
}
// 剖面 → 帘面(着色用 loadSection 返回的 s.scale与体的源色阶同源
loadingDs_.insert(dsId);
sceneRepo_.loadSection(
dsId,
[self, gen, dsId](data::SectionData s) {
if (!self) return;
self->loadingDs_.erase(dsId);
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return; // 作废/已取消
self->view_.addCurtain(dsId, s.grid, s.scale);
self->onDatasetArrived();
},
[self, gen, dsId](const std::string& m) {
if (!self) return;
self->loadingDs_.erase(dsId);
if (gen != self->rebuildGeneration_) return;
emit self->loadFailed(QString::fromStdString(m));
});
if (showVoxel_) {
auto cached = volumeCache_.find(dsId);
if (cached != volumeCache_.end()) {
view_.addVolume(dsId, cached->second, colorScale(dsId));
onDatasetArrived();
} else {
sceneRepo_.loadVolume(
dsId,
[self, gen, dsId](data::VolumeGrid g) {
if (!self || gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return;
auto it = self->volumeCache_.emplace(dsId, std::move(g)).first;
self->view_.addVolume(dsId, it->second, self->colorScale(dsId));
self->onDatasetArrived();
},
[self, gen](const std::string& m) {
if (!self || gen != self->rebuildGeneration_) return;
emit self->loadFailed(QString::fromStdString(m));
});
}
}
}
void VtkSceneController::onDatasetArrived() {

View File

@ -80,8 +80,6 @@ private:
std::map<std::string, geopro::core::Grid> gridCache_;
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
std::map<std::string, data::VolumeGrid> volumeCache_;
// 三维体色阶缓存mock 体在 dsRepo_ 无条目,色阶随 loadVolume 一起交付并缓存于此。
std::map<std::string, geopro::core::ColorScale> volumeScaleCache_;
// 异步回灌防护:每次全量 rebuild 自增,回调比对丢弃迟到结果。
unsigned long long rebuildGeneration_ = 0;

View File

@ -7,7 +7,6 @@ add_library(geopro_core STATIC
geo/CrsTransform.cpp
model/ColorScale.cpp
algo/IdwInterpolator.cpp
algo/VolumeBuilder.cpp
)
target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,65 +0,0 @@
#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) IDWmaxDist 外 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

View File

@ -1,26 +0,0 @@
#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 → ScalarVolumemaxDist 外 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

View File

@ -3,16 +3,9 @@
#include <QObject>
#include <QString>
#include <QVariant>
#include <cmath>
#include <cstddef>
#include <exception>
#include <memory>
#include <utility>
#include "algo/VolumeBuilder.hpp" // core::PointSet / BuiltVolume / buildVolume含 Field.hpp
#include "api/DatasetLoadHandles.hpp"
#include "model/ColorScale.hpp"
#include "model/detail/DetailPayloads.hpp"
#include "repo/IAsyncDatasetRepository.hpp"
@ -22,9 +15,7 @@ namespace {
constexpr const char* kNotReady = "后端三维端点未就绪";
} // namespace
Api3dRepository::Api3dRepository(IAsyncDatasetRepository& dsRepo,
std::shared_ptr<core::GeoLocalFrame> frame)
: dsRepo_(dsRepo), frame_(std::move(frame)) {}
Api3dRepository::Api3dRepository(IAsyncDatasetRepository& dsRepo) : dsRepo_(dsRepo) {}
DsDimension Api3dRepository::dimensionOf(const DsRow& ds) const {
// 与 LocalSample3dRepository::dimensionOf 同口径spec §6.1 ddCode→维度
@ -63,141 +54,9 @@ void Api3dRepository::loadSection(const std::string& dsId, std::function<void(Se
});
}
bool Api3dRepository::isVolumeDataset(const std::string& dsId) const {
return volumes_.find(dsId) != volumes_.end();
}
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::loadVolume(const std::string& /*dsId*/,
std::function<void(VolumeGrid)> /*onOk*/, OnError onErr) {
onErr(kNotReady); // 后端三维体端点未就绪
}
void Api3dRepository::loadTerrainPaths(std::function<void(TerrainPaths)> /*onOk*/, OnError onErr) {

View File

@ -1,18 +1,9 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "geo/GeoLocalFrame.hpp"
#include "repo/I3dSceneRepository.hpp"
#include "repo/VolumeBuildParams.hpp"
namespace geopro::core {
struct PointSet; // algo/IInterpolator.hppGrid/ColorScale 经 I3dSceneRepository.hpp 已可见)
} // namespace geopro::core
namespace geopro::data {
@ -28,21 +19,11 @@ class IAsyncDatasetRepository;
// 给用户明确"未实现"而非假成功。
class Api3dRepository : public I3dSceneRepository {
public:
// frame全项目共享 GeoLocalFrame与帘面/底图同一对象)——三维体散点按其 lat/lon→局部米
// 配准,保证与帘面构造性对齐(含运行期 reanchor
Api3dRepository(IAsyncDatasetRepository& dsRepo, std::shared_ptr<core::GeoLocalFrame> frame);
explicit Api3dRepository(IAsyncDatasetRepository& dsRepo);
DsDimension dimensionOf(const DsRow& ds) const override;
bool isVolumeDataset(const std::string& dsId) const override;
// ── 客户端创建三维体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,
void loadVolume(const std::string& dsId, std::function<void(VolumeGrid)> onOk,
OnError onErr) override;
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
OnError onErr) override;
@ -75,26 +56,7 @@ public:
OnError onErr) override;
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_;
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

View File

@ -51,16 +51,9 @@ public:
// 同步纯函数ds 类型 → 维度spec §6.1 映射表)。
virtual DsDimension dimensionOf(const DsRow& ds) const = 0;
// 同步纯函数:该 dsId 是否为三维体数据集 → 控制器据此选「体素」而非「帘面」渲染路径
// (取代旧的全局 showVoxel/showCurtain 图层开关,按数据集类型分流)。
virtual bool isVolumeDataset(const std::string& dsId) const = 0;
// 异步:加载三维体模型(成功回调 VolumeGrid + 其色阶,失败回调消息)。
// 色阶随体交付(= 源剖面色阶,与帘面一致):体的着色是其加载表示的固有部分,
// 不可由 dsId 经普通数据集仓储取(客户端 mock 体在后端无条目)。
// 异步:加载三维体模型(成功回调 VolumeGrid失败回调消息
virtual void loadVolume(const std::string& dsId,
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,
OnError onErr) = 0;
std::function<void(VolumeGrid)> onOk, OnError onErr) = 0;
// 异步:加载剖面(帘面)着色数据(Grid+色阶)。本地样本同步回调Api 实现走 ERT 反演端点异步回调。
virtual void loadSection(const std::string& dsId,

View File

@ -8,7 +8,7 @@
#include <utility>
#include <vector>
#include "algo/VolumeBuilder.hpp"
#include "algo/IdwInterpolator.hpp"
#include "geo/CrsTransform.hpp"
#include "geo/GeoLocalFrame.hpp"
#include "model/ColorScale.hpp"
@ -20,20 +20,31 @@ namespace geopro::data {
using geopro::core::ColorScale;
using geopro::core::CrsTransform;
using geopro::core::GeoLocalFrame;
using geopro::core::GridSpec;
using geopro::core::IdwInterpolator;
using geopro::core::PointSet;
using geopro::core::ScalarVolume;
using geopro::core::ScatterField;
namespace {
// 与 render::VoxelFromScatters 的默认参数同口径(保持渲染/切片纵向一致)。
// 「散点→GridSpec→IDW→ScalarVolume」已提到 core::buildVolume 共享(与 Api3dRepository 同源
// 消除调参漂移);此处仅保留默认参数 + 本地样本配准
// TODO(P2/P3): 与 render::buildVoxelFromScatters 的 cellXY/cellZ/power/maxDist 默认值重复
// 宜把"散点→配准→GridSpec→IDW→ScalarVolume"提到 core::algo 共享,避免单方调参静默不一致
constexpr double kCellXY = 1.0;
constexpr double kCellZ = 0.5;
constexpr double kPower = 2.0;
constexpr double kMaxDist = 4.0;
constexpr int kMaxDim = 400;
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
LocalSample3dRepository::LocalSample3dRepository(LocalSampleRepository& base, std::string projectCrs,
@ -54,9 +65,8 @@ DsDimension LocalSample3dRepository::dimensionOf(const DsRow& ds) const {
return DsDimension::Other;
}
void LocalSample3dRepository::loadVolume(
const std::string& /*dsId*/,
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk, OnError onErr) {
void LocalSample3dRepository::loadVolume(const std::string& /*dsId*/,
std::function<void(VolumeGrid)> onOk, OnError onErr) {
// P1 样本dsId 暂未使用,固定读同一组交叉剖面散点→体素(真实 Api 实现按 dsId 取)。
try {
// 1) 读两条交叉剖面散点 + 色阶;配准到世界局部米 + 深度,组装 IDW 输入点集。
@ -82,28 +92,55 @@ void LocalSample3dRepository::loadVolume(
return;
}
// 2) 点集 → GridSpec → IDW → 数据实测值域(共享 core::buildVolume
geopro::core::BuiltVolume bv =
geopro::core::buildVolume(pts, kCellXY, kCellZ, kPower, kMaxDist);
// 2) 点集包络 → GridSpec角点对齐
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]);
}
// 3) 值域:优先 colorBar 真实分段值,否则 buildVolume 的数据实测范围。
double vmin = bv.vmin, vmax = bv.vmax;
GridSpec spec{};
spec.ox = minx; spec.oy = miny; spec.oz = minz;
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 → ScalarVolumemaxDist 外 NaN 留空)。
const IdwInterpolator idw;
ScalarVolume vol = idw.interpolate(pts, spec);
// 4) 值域:优先 colorBar 真实分段值,否则数据实测。
double vmin, vmax;
ColorScale cs;
try {
cs = base_.loadScatterColorScale("grid1");
} catch (const std::exception&) {
// 色阶缺失 → 沿用数据实测范围。
// 色阶缺失 → 退化为数据实测范围。
}
const std::vector<double> stops = cs.stopValues();
if (stops.size() >= 2) {
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(bv.vol),
{{bv.spec.ox, bv.spec.oy, bv.spec.oz}},
{{bv.spec.dx, bv.spec.dy, bv.spec.dz}},
VolumeGrid out{std::move(vol),
{{spec.ox, spec.oy, spec.oz}},
{{spec.dx, spec.dy, spec.dz}},
vmin, vmax};
onOk(std::move(out), std::move(cs));
onOk(std::move(out));
} catch (const std::exception& e) {
onErr(std::string("LocalSample3dRepository::loadVolume: ") + e.what());
}

View File

@ -23,10 +23,7 @@ public:
double baseLat, double baseLon);
DsDimension dimensionOf(const DsRow& ds) const override;
// 本地样本无客户端创建的三维体(样本体经旧 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,
void loadVolume(const std::string& dsId, std::function<void(VolumeGrid)> onOk,
OnError onErr) override;
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
OnError onErr) override;

View File

@ -1,23 +0,0 @@
#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