feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
19 changed files with 720 additions and 98 deletions
Showing only changes of commit b261374cc9 - Show all commits

View File

@ -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. 异常:切片右键创建异常(圈定+保存对话框含截图)→ **接真实端点**

View File

@ -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` |
| 帘面/地形竖向 = +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

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

View File

@ -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

View File

@ -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

View File

@ -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点位置/字体本期 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);
@ -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(

View File

@ -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 显示;传播由组合根分发,避免重复发信号

View File

@ -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;

View File

@ -49,15 +49,27 @@ 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_.loadSection(
sceneRepo_.loadVolume(
dsId,
[self, gen, dsId](data::SectionData s) {
[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->view_.addCurtain(dsId, s.grid, s.scale);
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) {
@ -66,27 +78,26 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
if (gen != self->rebuildGeneration_) return;
emit self->loadFailed(QString::fromStdString(m));
});
return;
}
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));
});
}
}
// 剖面 → 帘面(着色用 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));
});
}
void VtkSceneController::onDatasetArrived() {

View File

@ -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;

View File

@ -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})

View File

@ -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) 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

@ -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 → 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,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) {

View File

@ -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.hppGrid/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

View File

@ -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,

View File

@ -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 → ScalarVolumemaxDist 外 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());
}

View File

@ -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;

View File

@ -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