feat(vtk): 客户端生成三维体流程(#1)-多源IDW插值+按类型分流体素/帘面
- core::buildVolume 共享管线(散点→GridSpec→IDW→值域),LocalSample/Api 同源,消除调参漂移 - VolumeBuildParams:参数必存(源ds+插值模型/参数+色阶);不冻结gridSpec,改用"源ds锁定"不变式(留校验TODO) - Api3dRepository:内存体存储 + createVolume/volumeRows/isVolumeDataset + 多源loadVolume (复用loadSection走inversion.grid,竖向=g.y高程,与帘面构造性对齐;绕开scatter端点y/z语义坑) - loadVolume 回调改交付(VolumeGrid, ColorScale):体色阶=源剖面色阶(mock体在dsRepo无条目) - UI:Column3DDataset(源数据栏)多选+右键「生成三维体」+ VolumeParamsDialog(IDW;克里金占位) - 生成的体归三维分析栏(Column3DAnalysis,设计§2.1),非数据集栏;main.cpp 两栏勾选聚合下发,体不被后端刷新冲掉 - VtkSceneController 按 isVolumeDataset 分流体素/帘面,取代失效的全局 showVoxel/showCurtain 编译链接绿(build.bat app exit 0);未GUI实测(Claude无法验VTK渲染)。设计/计划见 docs/superpowers/plans/2026-06-17-vtk-3d-volume-create-flow.md
This commit is contained in:
parent
4835528b99
commit
b261374cc9
|
|
@ -35,7 +35,10 @@
|
|||
|
||||
## 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 + 代码现状 + 实现拆解 + 持久化策略)。已与用户拍板的关键决策:
|
||||
|
|
@ -46,7 +49,7 @@
|
|||
- 数据模型层级:**三维体(源数据+插值参数) → 切片(属于体,保存后成 dd_slice) → 异常(画在切片平面,圈定保存)**,三者皆可持久化。
|
||||
|
||||
**实现拆解(设计文档 §6,按依赖排序)**:
|
||||
1. 三维体 mock 渲染:`Api3dRepository::loadVolume` 由 stub 改为"取源数据散点 → IDW → VolumeGrid → VoxelActor"。拆 `VolumeBuildParams`(含 GridSpec) 必存 / values 可选。
|
||||
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. 异常:切片右键创建异常(圈定+保存对话框含截图)→ **接真实端点**。
|
||||
|
|
|
|||
|
|
@ -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;若源真被改 ⇒ 体应失效重建,而非套旧锚点(旧锚点已是脏数据)。
|
||||
|
|
@ -64,6 +64,7 @@ add_executable(geopro_desktop WIN32
|
|||
ImportDatasetDialog.cpp
|
||||
ExportDatasetDialog.cpp
|
||||
SettingsDialog.cpp
|
||||
VolumeParamsDialog.cpp
|
||||
Logging.cpp
|
||||
DatasetDimension.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 "SettingsDialog.hpp"
|
||||
#include "TopBar.hpp"
|
||||
#include "VolumeParamsDialog.hpp"
|
||||
#include "ProjectListDialog.hpp"
|
||||
#include "ObjectFormDialog.hpp"
|
||||
#include "ImportDatasetDialog.hpp"
|
||||
|
|
@ -251,7 +252,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);
|
||||
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo, frame);
|
||||
auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr,
|
||||
frame, refElev);
|
||||
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
||||
|
|
@ -361,6 +362,27 @@ 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,
|
||||
|
|
@ -375,17 +397,39 @@ 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,
|
||||
&geopro::controller::VtkSceneController::setCheckedDatasets);
|
||||
[checkedProfiles, pushChecked](const QStringList& ids) {
|
||||
*checkedProfiles = ids;
|
||||
pushChecked();
|
||||
});
|
||||
// O点位置/字体本期 stub(TODO P4:弹框)。
|
||||
QObject::connect(c3, &geopro::app::Column3DDataset::oPointClicked, vtkWidget,
|
||||
[]() { /* TODO P4: O点位置弹框 */ });
|
||||
QObject::connect(c3, &geopro::app::Column3DDataset::fontClicked, vtkWidget,
|
||||
[]() { /* 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();
|
||||
// 三维分析栏勾选(三维体/切片)→ 并入渲染勾选集(体走体素路径,由 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);
|
||||
|
|
@ -629,24 +673,27 @@ 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](const QStringList& tmIds) {
|
||||
[&projectRepo, &nav, drawer, emptyState, generation, lastAnalysisRows,
|
||||
refreshAnalysis](const QStringList& tmIds) {
|
||||
const unsigned long long myGen = ++(*generation);
|
||||
emptyState->setVisible(tmIds.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染
|
||||
if (tmIds.isEmpty()) {
|
||||
drawer->col3D()->setDatasets({});
|
||||
drawer->col2D()->setDatasets({});
|
||||
drawer->colAnalysis()->setDatasets({});
|
||||
*lastAnalysisRows = {};
|
||||
refreshAnalysis(); // 后端分析行清空,但客户端三维体仍驻留三维分析栏
|
||||
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]() {
|
||||
auto finish = [acc, drawer, generation, myGen, lastAnalysisRows, refreshAnalysis]() {
|
||||
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
|
||||
geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc);
|
||||
drawer->col3D()->setDatasets(b.dim3D);
|
||||
drawer->col2D()->setDatasets(b.dim2D);
|
||||
drawer->colAnalysis()->setDatasets(b.analysis);
|
||||
*lastAnalysisRows = b.analysis;
|
||||
refreshAnalysis(); // 后端切片 + 客户端三维体合并注入三维分析栏
|
||||
};
|
||||
for (const QString& tm : tmIds) {
|
||||
geopro::data::NavRequest* req = projectRepo.loadRowsAsync(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@
|
|||
|
||||
#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>
|
||||
|
|
@ -111,10 +116,12 @@ 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;
|
||||
|
|
@ -124,9 +131,30 @@ 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 显示;传播由组合根分发,避免重复发信号
|
||||
|
|
|
|||
|
|
@ -30,8 +30,13 @@ 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;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,39 @@ 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);
|
||||
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);
|
||||
sceneRepo_.loadVolume(
|
||||
dsId,
|
||||
[self, gen, dsId](data::VolumeGrid g, core::ColorScale cs) {
|
||||
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]);
|
||||
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));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 剖面 → 帘面(着色用 loadSection 返回的 s.scale,与体的源色阶同源)。
|
||||
loadingDs_.insert(dsId);
|
||||
sceneRepo_.loadSection(
|
||||
dsId,
|
||||
|
|
@ -67,27 +99,6 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
|
|||
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_.renderIncremental();
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ 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;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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})
|
||||
|
|
|
|||
|
|
@ -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 <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"
|
||||
|
||||
|
|
@ -15,7 +22,9 @@ namespace {
|
|||
constexpr const char* kNotReady = "后端三维端点未就绪";
|
||||
} // 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 {
|
||||
// 与 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*/,
|
||||
std::function<void(VolumeGrid)> /*onOk*/, OnError onErr) {
|
||||
onErr(kNotReady); // 后端三维体端点未就绪
|
||||
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::loadTerrainPaths(std::function<void(TerrainPaths)> /*onOk*/, OnError onErr) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
#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.hpp(Grid/ColorScale 经 I3dSceneRepository.hpp 已可见)
|
||||
} // namespace geopro::core
|
||||
|
||||
namespace geopro::data {
|
||||
|
||||
|
|
@ -19,11 +28,21 @@ class IAsyncDatasetRepository;
|
|||
// 给用户明确"未实现"而非假成功。
|
||||
class Api3dRepository : public I3dSceneRepository {
|
||||
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;
|
||||
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;
|
||||
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
|
||||
OnError onErr) override;
|
||||
|
|
@ -56,7 +75,26 @@ 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
|
||||
|
|
|
|||
|
|
@ -51,9 +51,16 @@ public:
|
|||
// 同步纯函数:ds 类型 → 维度(spec §6.1 映射表)。
|
||||
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,
|
||||
std::function<void(VolumeGrid)> onOk, OnError onErr) = 0;
|
||||
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,
|
||||
OnError onErr) = 0;
|
||||
|
||||
// 异步:加载剖面(帘面)着色数据(Grid+色阶)。本地样本同步回调;Api 实现走 ERT 反演端点异步回调。
|
||||
virtual void loadSection(const std::string& dsId,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "algo/IdwInterpolator.hpp"
|
||||
#include "algo/VolumeBuilder.hpp"
|
||||
#include "geo/CrsTransform.hpp"
|
||||
#include "geo/GeoLocalFrame.hpp"
|
||||
#include "model/ColorScale.hpp"
|
||||
|
|
@ -20,31 +20,20 @@ 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 的默认参数同口径(保持渲染/切片纵向一致)。
|
||||
// TODO(P2/P3): 与 render::buildVoxelFromScatters 的 cellXY/cellZ/power/maxDist 默认值重复,
|
||||
// 宜把"散点→配准→GridSpec→IDW→ScalarVolume"提到 core::algo 共享,避免单方调参静默不一致。
|
||||
// 「散点→GridSpec→IDW→ScalarVolume」已提到 core::buildVolume 共享(与 Api3dRepository 同源,
|
||||
// 消除调参漂移);此处仅保留默认参数 + 本地样本配准。
|
||||
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,
|
||||
|
|
@ -65,8 +54,9 @@ DsDimension LocalSample3dRepository::dimensionOf(const DsRow& ds) const {
|
|||
return DsDimension::Other;
|
||||
}
|
||||
|
||||
void LocalSample3dRepository::loadVolume(const std::string& /*dsId*/,
|
||||
std::function<void(VolumeGrid)> onOk, OnError onErr) {
|
||||
void LocalSample3dRepository::loadVolume(
|
||||
const std::string& /*dsId*/,
|
||||
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk, OnError onErr) {
|
||||
// P1 样本:dsId 暂未使用,固定读同一组交叉剖面散点→体素(真实 Api 实现按 dsId 取)。
|
||||
try {
|
||||
// 1) 读两条交叉剖面散点 + 色阶;配准到世界局部米 + 深度,组装 IDW 输入点集。
|
||||
|
|
@ -92,55 +82,28 @@ void LocalSample3dRepository::loadVolume(const std::string& /*dsId*/,
|
|||
return;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
}
|
||||
// 2) 点集 → GridSpec → IDW → 数据实测值域(共享 core::buildVolume)。
|
||||
geopro::core::BuiltVolume bv =
|
||||
geopro::core::buildVolume(pts, kCellXY, kCellZ, kPower, kMaxDist);
|
||||
|
||||
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 → ScalarVolume(maxDist 外 NaN 留空)。
|
||||
const IdwInterpolator idw;
|
||||
ScalarVolume vol = idw.interpolate(pts, spec);
|
||||
|
||||
// 4) 值域:优先 colorBar 真实分段值,否则数据实测。
|
||||
double vmin, vmax;
|
||||
// 3) 值域:优先 colorBar 真实分段值,否则 buildVolume 的数据实测范围。
|
||||
double vmin = bv.vmin, vmax = bv.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(vol),
|
||||
{{spec.ox, spec.oy, spec.oz}},
|
||||
{{spec.dx, spec.dy, spec.dz}},
|
||||
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};
|
||||
onOk(std::move(out));
|
||||
onOk(std::move(out), std::move(cs));
|
||||
} catch (const std::exception& e) {
|
||||
onErr(std::string("LocalSample3dRepository::loadVolume: ") + e.what());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ public:
|
|||
double baseLat, double baseLon);
|
||||
|
||||
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;
|
||||
void loadSection(const std::string& dsId, std::function<void(SectionData)> onOk,
|
||||
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