11 KiB
实现计划:客户端「生成三维体」完整流程(#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):
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):
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 存储(成员):
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-NdsId、存入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 保证与帘面同系对齐):
- 查
volumes_[dsId];无 → onErr。 - 有
cachedGrid→ 直接 onOk(明细命中)。 - 否则:对每个 sourceId 调
this->loadSection(srcId)(即inversion.grid路径),shared_ptr<Agg>聚合 N 个回调(全到齐再继续;任一 failed → 整体 onErr 一次)。 appendGridPoints:对每个源 Grid 的有限值节点,按 CurtainActor 同口径定位 →frame.toLocal(g.lat[i], g.lon[i])作 (x,y);世界 Z = g.y[j](高程);v = g.valueAt(i,j)。跳过 NaN 格(与帘面消隐一致)。- 取首个到达源的色阶定 vmin/vmax(否则 buildVolume 数据实测)。
core::buildVolume(pts, cellXY/cellZ/power/maxDist)(每次从源确定性重算 gridSpec)。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;若源真被改 ⇒ 体应失效重建,而非套旧锚点(旧锚点已是脏数据)。