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