139 lines
11 KiB
Markdown
139 lines
11 KiB
Markdown
# 实现计划:客户端「生成三维体」完整流程(#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;若源真被改 ⇒ 体应失效重建,而非套旧锚点(旧锚点已是脏数据)。
|