feat/vtk-3d-view #7
|
|
@ -0,0 +1,116 @@
|
||||||
|
# 设计:三维体/切片 数据详情(只读属性对话框)
|
||||||
|
|
||||||
|
> 日期 2026-06-18。分支 `feat/vtk-3d-view`。收尾/打磨项 #6(见 `docs/superpowers/HANDOFF-vtk-3d.md` §4 末「下一步候选」)。
|
||||||
|
> 异常详情已用对话框做掉(`AnomalyPropertiesDialog`),本设计为**三维体 / 切片**补同类只读详情。
|
||||||
|
|
||||||
|
## 1. 目标与范围
|
||||||
|
|
||||||
|
三维分析栏右键「数据详情」时,弹出只读属性对话框展示该三维体 / 切片的元数据与统计。
|
||||||
|
- **形态**:只读 `QDialog`(仿 `AnomalyPropertiesDialog`),非停靠面板页签。
|
||||||
|
- 取舍理由:现成 `DatasetDetailController/Panel` 绑定 2D 的 `IAsyncDatasetRepository` + chartRegistry,而体/切片数据在 `Api3dRepository`(独立 3D 仓储),硬接需跨仓储桥接 + 新策略/视图,代价大、动共享设施风险高。对话框与刚落地的异常详情 UX 一致、零侵入 2D 管线。
|
||||||
|
- **内容范围**:参数/位姿随时可取;三维体统计(值域/测点数/范围)体被生成(loadVolume 缓存)后才显示,未生成显「—(生成/渲染后可见)」。
|
||||||
|
|
||||||
|
## 2. 架构与新增文件
|
||||||
|
|
||||||
|
仿 `src/app/AnomalyPropertiesDialog.{hpp,cpp}`,`QFormLayout` + `QLabel` 只读表:
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `src/app/VolumePropertiesDialog.{hpp,cpp}` | 三维体属性(参数 + 统计) |
|
||||||
|
| `src/app/SlicePropertiesDialog.{hpp,cpp}` | 切片属性(位姿 + 参数) |
|
||||||
|
|
||||||
|
两个对话框各自独立、构造即填充、`exec()` 模态,无网络、无加载态。
|
||||||
|
|
||||||
|
## 3. 数据获取
|
||||||
|
|
||||||
|
只改具体类 `src/data/api/Api3dRepository.{hpp,cpp}`;**接口 `I3dSceneRepository` 与 `LocalSample3dRepository` 不动**(`main.cpp` 持有具体 `scene3dRepo`,见 main.cpp:266,全程直接用)。
|
||||||
|
|
||||||
|
### 3.1 三维体 getter(新增)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Api3dRepository.hpp 内嵌结构 + 方法
|
||||||
|
struct VolumeInfo {
|
||||||
|
VolumeBuildParams params;
|
||||||
|
std::string name;
|
||||||
|
bool loaded = false; // cachedGrid 是否已就绪(= loadVolume 跑过)
|
||||||
|
// 以下仅 loaded 时有效:
|
||||||
|
double vmin = 0.0, vmax = 0.0; // 来自 cachedGrid
|
||||||
|
int nx = 0, ny = 0, nz = 0; // 网格维度
|
||||||
|
double dx = 0, dy = 0, dz = 0; // 单元间距(来自 cachedGrid.spacing)
|
||||||
|
std::size_t pointCount = 0; // 聚合后参与插值的散点数
|
||||||
|
};
|
||||||
|
bool volumeInfo(const std::string& dsId, VolumeInfo& out) const; // 非体返回 false
|
||||||
|
```
|
||||||
|
|
||||||
|
- `loaded` 取 `StoredVolume::cachedGrid.has_value()`;统计字段从 `cachedGrid`(vmin/vmax、`vol.nx()/ny()/nz()`、`spacing`)填。
|
||||||
|
- **测点数持久化**:`StoredVolume` 增 `std::optional<std::size_t> pointCount`,在 `finalizeVolume`(散点聚合完成处)写入 `pts.v.size()`。`volumeInfo` 透出。
|
||||||
|
|
||||||
|
### 3.2 切片数据
|
||||||
|
|
||||||
|
复用已有 `bool sliceSpec(const std::string& dsId, SliceSpec& out) const`(main.cpp 已在用)取位姿;名称用 `detailRequested` 信号已携带的 `name`,不新增 getter。
|
||||||
|
|
||||||
|
## 4. 触发与接线(`main.cpp`)
|
||||||
|
|
||||||
|
`detailRequested` 仅来自三维分析栏(`Column3DAnalysis`,项非体即切片;右键菜单「数据详情」已接,无需改 Column3DAnalysis),现连接 `detailCtrl.openDataset`(对 3D dsId 会降级失败)。改为按 ddCode 分派:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QObject::connect(ca, &Column3DAnalysis::detailRequested, &window,
|
||||||
|
[&window, scene3dRepo](const QString& dsId, const QString& ddCode, const QString& name) {
|
||||||
|
if (ddCode == QStringLiteral("dd_slice")) {
|
||||||
|
I3dSceneRepository::SliceSpec sp;
|
||||||
|
if (scene3dRepo->sliceSpec(dsId.toStdString(), sp)) {
|
||||||
|
SlicePropertiesDialog dlg(name, sp, &window); dlg.exec();
|
||||||
|
}
|
||||||
|
} else { // dd_voxel
|
||||||
|
Api3dRepository::VolumeInfo info;
|
||||||
|
if (scene3dRepo->volumeInfo(dsId.toStdString(), info)) {
|
||||||
|
VolumePropertiesDialog dlg(name, info, &window); dlg.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/app/CMakeLists.txt` 加两个新 `.cpp`。
|
||||||
|
|
||||||
|
## 5. 内容字段
|
||||||
|
|
||||||
|
### 三维体(`VolumePropertiesDialog`)
|
||||||
|
- 名称
|
||||||
|
- 源数据集(`sourceDatasetIds`,逗号连接)
|
||||||
|
- 插值模型(IDW / Kriging)+ 幂指数(IDW 时显 `power`)
|
||||||
|
- 网格间距(`XY=cellXY m Z=cellZ m`)
|
||||||
|
- 超距(`maxDist m`)
|
||||||
|
- 色阶来源(`colorScaleId`,空显「首个源数据集」)
|
||||||
|
- **统计**(loaded 才有,否则全显「—(生成/渲染后可见)」):
|
||||||
|
- 值域(`vmin ~ vmax`)
|
||||||
|
- 网格(`nx × ny × nz`)
|
||||||
|
- 测点数(`pointCount`)
|
||||||
|
- 范围(`nx·dx × ny·dy × nz·dz` 米)
|
||||||
|
|
||||||
|
### 切片(`SlicePropertiesDialog`)
|
||||||
|
- 名称
|
||||||
|
- 所属三维体(`volumeDsId`)
|
||||||
|
- 轴向(0 上下 / 1 前后 / 2 左右 / 3 任意)
|
||||||
|
- 平面三点 Origin / Point1 / Point2(各 `(x, y, z)` 米,2 位小数)
|
||||||
|
- 色阶来源(`colorScaleId`,空显「首个源数据集」)
|
||||||
|
|
||||||
|
> 切片**不含统计项**:采样分辨率/值域来自渲染时的切面网格,仓储层不持久化(`StoredSlice` 仅存 `spec`+`name`)。回写渲染产物属额外 plumbing,守 YAGNI 不做。位姿/参数已完整。
|
||||||
|
|
||||||
|
## 6. 错误处理
|
||||||
|
|
||||||
|
- `volumeInfo` / `sliceSpec` 取不到(非体/非切片)→ 返回 false,不弹空对话框(理论不发生,触发来自该行)。
|
||||||
|
- 统计未就绪 → 占位「—(生成/渲染后可见)」,不报错。
|
||||||
|
|
||||||
|
## 7. 测试
|
||||||
|
|
||||||
|
- 新增 gtest(`tests/` 内 Api3dRepository 测套,若无则新建)覆盖 `volumeInfo`:
|
||||||
|
- `createVolume` 后、`loadVolume` 前:`volumeInfo` 返回 true、`params`/`name` 正确、`loaded=false`、`pointCount=0`。
|
||||||
|
- `loadVolume` 成功后:`loaded=true`、`vmin<vmax`、`nx/ny/nz>0`、`pointCount>0`。
|
||||||
|
- 非体 dsId:返回 false。
|
||||||
|
- 对话框为纯只读 UI(无逻辑分支),不做单测,靠 GUI 实测(Claude 无法 GUI 验证,交用户)。
|
||||||
|
|
||||||
|
## 8. 影响面 / 不变量
|
||||||
|
|
||||||
|
- 接口 `I3dSceneRepository` 与 `LocalSample3dRepository` 零改动 → 真实后端就绪后切换不受影响。
|
||||||
|
- `finalizeVolume` 仅多写一个 `pointCount`,不改插值/渲染行为。
|
||||||
|
- 不与 VTK 三维视图交互(详情只读查阅,职责清晰)。
|
||||||
|
|
@ -68,7 +68,9 @@ add_executable(geopro_desktop WIN32
|
||||||
AnomalyPropertiesDialog.cpp
|
AnomalyPropertiesDialog.cpp
|
||||||
SettingsDialog.cpp
|
SettingsDialog.cpp
|
||||||
SliceExport.cpp
|
SliceExport.cpp
|
||||||
|
SlicePropertiesDialog.cpp
|
||||||
VolumeParamsDialog.cpp
|
VolumeParamsDialog.cpp
|
||||||
|
VolumePropertiesDialog.cpp
|
||||||
Logging.cpp
|
Logging.cpp
|
||||||
DatasetDimension.cpp
|
DatasetDimension.cpp
|
||||||
TileBasemap.cpp)
|
TileBasemap.cpp)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
#include "SlicePropertiesDialog.hpp"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using SliceSpec = geopro::data::I3dSceneRepository::SliceSpec;
|
||||||
|
|
||||||
|
QString axisLabel(int axis) {
|
||||||
|
switch (axis) {
|
||||||
|
case 0: return QStringLiteral("上下");
|
||||||
|
case 1: return QStringLiteral("前后");
|
||||||
|
case 2: return QStringLiteral("左右");
|
||||||
|
case 3: return QStringLiteral("任意");
|
||||||
|
default: return QStringLiteral("—");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pointLabel(const std::array<double, 3>& p) {
|
||||||
|
return QStringLiteral("(%1, %2, %3)")
|
||||||
|
.arg(p[0], 0, 'f', 2)
|
||||||
|
.arg(p[1], 0, 'f', 2)
|
||||||
|
.arg(p[2], 0, 'f', 2);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SlicePropertiesDialog::SlicePropertiesDialog(const QString& name, const SliceSpec& spec,
|
||||||
|
QWidget* parent)
|
||||||
|
: QDialog(parent) {
|
||||||
|
setWindowTitle(QStringLiteral("切片属性"));
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
|
||||||
|
form->addRow(QStringLiteral("名称"),
|
||||||
|
new QLabel(name.isEmpty() ? QStringLiteral("—") : name));
|
||||||
|
form->addRow(QStringLiteral("所属三维体"),
|
||||||
|
new QLabel(spec.volumeDsId.empty() ? QStringLiteral("—")
|
||||||
|
: QString::fromStdString(spec.volumeDsId)));
|
||||||
|
form->addRow(QStringLiteral("轴向"), new QLabel(axisLabel(spec.axis)));
|
||||||
|
form->addRow(QStringLiteral("Origin"), new QLabel(pointLabel(spec.origin)));
|
||||||
|
form->addRow(QStringLiteral("Point1"), new QLabel(pointLabel(spec.point1)));
|
||||||
|
form->addRow(QStringLiteral("Point2"), new QLabel(pointLabel(spec.point2)));
|
||||||
|
form->addRow(QStringLiteral("色阶来源"),
|
||||||
|
new QLabel(spec.colorScaleId.empty()
|
||||||
|
? QStringLiteral("首个源数据集")
|
||||||
|
: QString::fromStdString(spec.colorScaleId)));
|
||||||
|
|
||||||
|
root->addLayout(form);
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
root->addWidget(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "repo/I3dSceneRepository.hpp" // I3dSceneRepository::SliceSpec
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 切片属性对话框(收尾项 #6):三维分析栏右键「数据详情」弹出,只读展示切片的
|
||||||
|
// 位姿/参数(所属三维体/轴向/平面三点/色阶)。
|
||||||
|
// 不含采样分辨率/值域等统计:切面网格来自渲染时计算、仓储层不持久化(守 YAGNI)。
|
||||||
|
class SlicePropertiesDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
SlicePropertiesDialog(const QString& name,
|
||||||
|
const geopro::data::I3dSceneRepository::SliceSpec& spec,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
#include "VolumePropertiesDialog.hpp"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using VolumeInfo = geopro::data::Api3dRepository::VolumeInfo;
|
||||||
|
using Model = geopro::data::VolumeBuildParams::Model;
|
||||||
|
|
||||||
|
constexpr const char* kPending = "—(生成/渲染后可见)";
|
||||||
|
|
||||||
|
QString joinSources(const std::vector<std::string>& ids) {
|
||||||
|
if (ids.empty()) return QStringLiteral("—");
|
||||||
|
QStringList list;
|
||||||
|
for (const auto& s : ids) list << QString::fromStdString(s);
|
||||||
|
return list.join(QStringLiteral(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString modelLabel(const geopro::data::VolumeBuildParams& p) {
|
||||||
|
if (p.interpModel == Model::Idw)
|
||||||
|
return QStringLiteral("IDW(幂=%1)").arg(p.power, 0, 'f', 1);
|
||||||
|
return QStringLiteral("Kriging");
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
VolumePropertiesDialog::VolumePropertiesDialog(const QString& name, const VolumeInfo& info,
|
||||||
|
QWidget* parent)
|
||||||
|
: QDialog(parent) {
|
||||||
|
setWindowTitle(QStringLiteral("三维体属性"));
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
|
||||||
|
// ── 参数(随时可取)─────────────────────────────────────────────
|
||||||
|
form->addRow(QStringLiteral("名称"),
|
||||||
|
new QLabel(name.isEmpty() ? QStringLiteral("—") : name));
|
||||||
|
form->addRow(QStringLiteral("源数据集"), new QLabel(joinSources(info.params.sourceDatasetIds)));
|
||||||
|
form->addRow(QStringLiteral("插值模型"), new QLabel(modelLabel(info.params)));
|
||||||
|
form->addRow(QStringLiteral("网格间距"),
|
||||||
|
new QLabel(QStringLiteral("XY=%1 m Z=%2 m")
|
||||||
|
.arg(info.params.cellXY, 0, 'f', 2)
|
||||||
|
.arg(info.params.cellZ, 0, 'f', 2)));
|
||||||
|
form->addRow(QStringLiteral("超距"),
|
||||||
|
new QLabel(QStringLiteral("%1 m").arg(info.params.maxDist, 0, 'f', 2)));
|
||||||
|
form->addRow(QStringLiteral("色阶来源"),
|
||||||
|
new QLabel(info.params.colorScaleId.empty()
|
||||||
|
? QStringLiteral("首个源数据集")
|
||||||
|
: QString::fromStdString(info.params.colorScaleId)));
|
||||||
|
|
||||||
|
// ── 统计(仅 loaded 时有效)──────────────────────────────────────
|
||||||
|
if (info.loaded) {
|
||||||
|
form->addRow(QStringLiteral("值域"), new QLabel(QStringLiteral("%1 ~ %2")
|
||||||
|
.arg(info.vmin, 0, 'f', 2)
|
||||||
|
.arg(info.vmax, 0, 'f', 2)));
|
||||||
|
form->addRow(QStringLiteral("网格"), new QLabel(QStringLiteral("%1 × %2 × %3")
|
||||||
|
.arg(info.nx)
|
||||||
|
.arg(info.ny)
|
||||||
|
.arg(info.nz)));
|
||||||
|
form->addRow(QStringLiteral("测点数"),
|
||||||
|
new QLabel(QString::number(static_cast<qulonglong>(info.pointCount))));
|
||||||
|
form->addRow(QStringLiteral("范围"),
|
||||||
|
new QLabel(QStringLiteral("%1 × %2 × %3 m")
|
||||||
|
.arg(info.nx * info.dx, 0, 'f', 1)
|
||||||
|
.arg(info.ny * info.dy, 0, 'f', 1)
|
||||||
|
.arg(info.nz * info.dz, 0, 'f', 1)));
|
||||||
|
} else {
|
||||||
|
form->addRow(QStringLiteral("统计"), new QLabel(QString::fromUtf8(kPending)));
|
||||||
|
}
|
||||||
|
|
||||||
|
root->addLayout(form);
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
root->addWidget(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "api/Api3dRepository.hpp" // Api3dRepository::VolumeInfo
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 三维体属性对话框(收尾项 #6):三维分析栏右键「数据详情」弹出,只读展示三维体的
|
||||||
|
// 参数(源数据/插值模型/网格/超距/色阶)与统计(值域/网格/测点数/范围)。
|
||||||
|
// 统计仅在体被生成过(loadVolume 缓存明细,info.loaded=true)时显示,否则显占位。
|
||||||
|
class VolumePropertiesDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
VolumePropertiesDialog(const QString& name,
|
||||||
|
const geopro::data::Api3dRepository::VolumeInfo& info,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -100,9 +100,11 @@
|
||||||
#include "AnomalySaveDialog.hpp"
|
#include "AnomalySaveDialog.hpp"
|
||||||
#include "AnomalyPropertiesDialog.hpp"
|
#include "AnomalyPropertiesDialog.hpp"
|
||||||
#include "SettingsDialog.hpp"
|
#include "SettingsDialog.hpp"
|
||||||
|
#include "SlicePropertiesDialog.hpp"
|
||||||
#include "SliceExport.hpp"
|
#include "SliceExport.hpp"
|
||||||
#include "TopBar.hpp"
|
#include "TopBar.hpp"
|
||||||
#include "VolumeParamsDialog.hpp"
|
#include "VolumeParamsDialog.hpp"
|
||||||
|
#include "VolumePropertiesDialog.hpp"
|
||||||
#include "interact/AnomalyDrawTool.hpp"
|
#include "interact/AnomalyDrawTool.hpp"
|
||||||
#include "ProjectListDialog.hpp"
|
#include "ProjectListDialog.hpp"
|
||||||
#include "ObjectFormDialog.hpp"
|
#include "ObjectFormDialog.hpp"
|
||||||
|
|
@ -682,9 +684,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
[interactionMgr](geopro::render::interact::SliceAxis axis) {
|
[interactionMgr](geopro::render::interact::SliceAxis axis) {
|
||||||
interactionMgr->addSlice(axis);
|
interactionMgr->addSlice(axis);
|
||||||
});
|
});
|
||||||
QObject::connect(ca, &geopro::app::Column3DAnalysis::detailRequested, &detailCtrl,
|
// 三维分析栏「数据详情」:项非体即切片(dd_slice / dd_voxel),按 ddCode 分派到只读属性
|
||||||
[&detailCtrl](const QString& dsId, const QString& ddCode, const QString& name) {
|
// 对话框(仿异常详情)。数据直接从具体 scene3dRepo 取(体/切片在 3D 仓储,非 detailCtrl 的 2D 管线)。
|
||||||
detailCtrl.openDataset(dsId, ddCode, name);
|
QObject::connect(ca, &geopro::app::Column3DAnalysis::detailRequested, &window,
|
||||||
|
[&window, scene3dRepo](const QString& dsId, const QString& ddCode,
|
||||||
|
const QString& name) {
|
||||||
|
if (ddCode == QStringLiteral("dd_slice")) {
|
||||||
|
geopro::data::I3dSceneRepository::SliceSpec sp;
|
||||||
|
if (scene3dRepo->sliceSpec(dsId.toStdString(), sp)) {
|
||||||
|
geopro::app::SlicePropertiesDialog dlg(name, sp, &window);
|
||||||
|
dlg.exec();
|
||||||
|
}
|
||||||
|
} else { // dd_voxel:三维体
|
||||||
|
geopro::data::Api3dRepository::VolumeInfo info;
|
||||||
|
if (scene3dRepo->volumeInfo(dsId.toStdString(), info)) {
|
||||||
|
geopro::app::VolumePropertiesDialog dlg(name, info, &window);
|
||||||
|
dlg.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// 三维分析栏切片右键「删除」→ 删除 mock 切片 + 刷新列表(若在渲染,删后行消失→取消勾选→自动移除图元)。
|
// 三维分析栏切片右键「删除」→ 删除 mock 切片 + 刷新列表(若在渲染,删后行消失→取消勾选→自动移除图元)。
|
||||||
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceDeleteRequested, &window,
|
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceDeleteRequested, &window,
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,29 @@ std::vector<DsRow> Api3dRepository::volumeRows() const {
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Api3dRepository::volumeInfo(const std::string& dsId, VolumeInfo& out) const {
|
||||||
|
auto it = volumes_.find(dsId);
|
||||||
|
if (it == volumes_.end()) return false;
|
||||||
|
const StoredVolume& sv = it->second;
|
||||||
|
out = VolumeInfo{};
|
||||||
|
out.params = sv.params;
|
||||||
|
out.name = sv.name;
|
||||||
|
out.loaded = sv.cachedGrid.has_value();
|
||||||
|
if (out.loaded) {
|
||||||
|
const VolumeGrid& g = *sv.cachedGrid;
|
||||||
|
out.vmin = g.vmin;
|
||||||
|
out.vmax = g.vmax;
|
||||||
|
out.nx = g.vol.nx();
|
||||||
|
out.ny = g.vol.ny();
|
||||||
|
out.nz = g.vol.nz();
|
||||||
|
out.dx = g.spacing[0];
|
||||||
|
out.dy = g.spacing[1];
|
||||||
|
out.dz = g.spacing[2];
|
||||||
|
out.pointCount = sv.pointCount.value_or(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void Api3dRepository::appendGridPoints(const core::Grid& g, core::PointSet& pts) const {
|
void Api3dRepository::appendGridPoints(const core::Grid& g, core::PointSet& pts) const {
|
||||||
const int nx = g.nx(), ny = g.ny();
|
const int nx = g.nx(), ny = g.ny();
|
||||||
if (nx < 1 || ny < 1 || g.y.size() < static_cast<std::size_t>(ny)) return;
|
if (nx < 1 || ny < 1 || g.y.size() < static_cast<std::size_t>(ny)) return;
|
||||||
|
|
@ -146,6 +169,7 @@ void Api3dRepository::finalizeVolume(const std::string& dsId, const core::PointS
|
||||||
if (it != volumes_.end()) { // 缓存明细 + 色阶(下次命中即跳重算)
|
if (it != volumes_.end()) { // 缓存明细 + 色阶(下次命中即跳重算)
|
||||||
it->second.cachedGrid = out;
|
it->second.cachedGrid = out;
|
||||||
it->second.cachedScale = scale;
|
it->second.cachedScale = scale;
|
||||||
|
it->second.pointCount = pts.v.size(); // 持久化聚合散点数(详情统计用)
|
||||||
}
|
}
|
||||||
onOk(std::move(out), scale);
|
onOk(std::move(out), scale);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -40,6 +41,20 @@ public:
|
||||||
std::string createVolume(VolumeBuildParams params, const std::string& name);
|
std::string createVolume(VolumeBuildParams params, const std::string& name);
|
||||||
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维分析栏合并注入(每次 setDatasets 追加)。
|
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维分析栏合并注入(每次 setDatasets 追加)。
|
||||||
std::vector<DsRow> volumeRows() const;
|
std::vector<DsRow> volumeRows() const;
|
||||||
|
|
||||||
|
// 三维体只读详情(属性对话框用):参数随时可取;统计(值域/网格/测点数/范围)仅
|
||||||
|
// loaded(loadVolume 缓存过明细)时有效,未加载 loaded=false、统计字段全 0。
|
||||||
|
struct VolumeInfo {
|
||||||
|
VolumeBuildParams params;
|
||||||
|
std::string name;
|
||||||
|
bool loaded = false; // cachedGrid 是否就绪(= loadVolume 跑过)
|
||||||
|
double vmin = 0.0, vmax = 0.0; // 以下仅 loaded 时有效:
|
||||||
|
int nx = 0, ny = 0, nz = 0; // 网格维度
|
||||||
|
double dx = 0.0, dy = 0.0, dz = 0.0; // 单元间距
|
||||||
|
std::size_t pointCount = 0; // 聚合后参与插值的散点数
|
||||||
|
};
|
||||||
|
// 取回三维体详情;dsId 非三维体返回 false(不弹空对话框)。
|
||||||
|
bool volumeInfo(const std::string& dsId, VolumeInfo& out) const;
|
||||||
// 已保存切片的列表行(ddCode="dd_slice",parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。
|
// 已保存切片的列表行(ddCode="dd_slice",parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。
|
||||||
std::vector<DsRow> sliceRows() const;
|
std::vector<DsRow> sliceRows() const;
|
||||||
// 该 dsId 是否为已保存切片(3b:分析栏勾选 dd_slice 走切片重渲染路径,不进控制器帘面/体素路径)。
|
// 该 dsId 是否为已保存切片(3b:分析栏勾选 dd_slice 走切片重渲染路径,不进控制器帘面/体素路径)。
|
||||||
|
|
@ -98,6 +113,7 @@ private:
|
||||||
std::string name;
|
std::string name;
|
||||||
std::optional<VolumeGrid> cachedGrid;
|
std::optional<VolumeGrid> cachedGrid;
|
||||||
core::ColorScale cachedScale; // 与 cachedGrid 同时填(源剖面色阶)
|
core::ColorScale cachedScale; // 与 cachedGrid 同时填(源剖面色阶)
|
||||||
|
std::optional<std::size_t> pointCount; // 聚合散点数(finalizeVolume 时持久化,详情统计用)
|
||||||
};
|
};
|
||||||
std::map<std::string, StoredVolume> volumes_; // dsId → 体
|
std::map<std::string, StoredVolume> volumes_; // dsId → 体
|
||||||
int volumeCounter_ = 0;
|
int volumeCounter_ = 0;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "api/Api3dRepository.hpp"
|
||||||
|
#include "geo/GeoLocalFrame.hpp"
|
||||||
#include "repo/I3dSceneRepository.hpp"
|
#include "repo/I3dSceneRepository.hpp"
|
||||||
|
#include "repo/IAsyncDatasetRepository.hpp"
|
||||||
#include "repo/LocalSample3dRepository.hpp"
|
#include "repo/LocalSample3dRepository.hpp"
|
||||||
#include "repo/LocalSampleRepository.hpp"
|
#include "repo/LocalSampleRepository.hpp"
|
||||||
|
#include "repo/VolumeBuildParams.hpp"
|
||||||
|
|
||||||
using namespace geopro::data;
|
using namespace geopro::data;
|
||||||
|
|
||||||
|
|
@ -69,3 +74,51 @@ TEST(LocalSample3dRepo, LoadTerrainPathsCallsBack) {
|
||||||
EXPECT_FALSE(got.demPath.empty());
|
EXPECT_FALSE(got.demPath.empty());
|
||||||
EXPECT_FALSE(got.imagePath.empty());
|
EXPECT_FALSE(got.imagePath.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 极简桩:volumeInfo/createVolume 不触碰 dsRepo_,loadAsync 直接回空。
|
||||||
|
struct StubAsyncRepo : IAsyncDatasetRepository {
|
||||||
|
DetailLoad* loadAsync(const std::string&, const std::string&, int, int) override {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// volumeInfo:createVolume 后、loadVolume 前 → 返回 true,参数/名称正确,loaded=false、无测点数。
|
||||||
|
TEST(Api3dRepo, VolumeInfoBeforeLoad) {
|
||||||
|
StubAsyncRepo dsRepo;
|
||||||
|
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(22.0, 114.0);
|
||||||
|
Api3dRepository repo(dsRepo, frame);
|
||||||
|
|
||||||
|
VolumeBuildParams p;
|
||||||
|
p.sourceDatasetIds = {"src-a", "src-b"};
|
||||||
|
p.interpModel = VolumeBuildParams::Model::Idw;
|
||||||
|
p.cellXY = 2.0;
|
||||||
|
p.cellZ = 0.5;
|
||||||
|
p.power = 3.0;
|
||||||
|
p.maxDist = 5.0;
|
||||||
|
p.colorScaleId = "src-a";
|
||||||
|
const std::string id = repo.createVolume(p, "体A");
|
||||||
|
|
||||||
|
Api3dRepository::VolumeInfo info;
|
||||||
|
ASSERT_TRUE(repo.volumeInfo(id, info));
|
||||||
|
EXPECT_EQ(info.name, "体A");
|
||||||
|
EXPECT_FALSE(info.loaded);
|
||||||
|
EXPECT_EQ(info.pointCount, 0u);
|
||||||
|
ASSERT_EQ(info.params.sourceDatasetIds.size(), 2u);
|
||||||
|
EXPECT_EQ(info.params.sourceDatasetIds[0], "src-a");
|
||||||
|
EXPECT_DOUBLE_EQ(info.params.cellXY, 2.0);
|
||||||
|
EXPECT_DOUBLE_EQ(info.params.power, 3.0);
|
||||||
|
EXPECT_DOUBLE_EQ(info.params.maxDist, 5.0);
|
||||||
|
EXPECT_EQ(info.params.colorScaleId, "src-a");
|
||||||
|
}
|
||||||
|
|
||||||
|
// volumeInfo:未知 dsId(非三维体)→ 返回 false,不弹空对话框。
|
||||||
|
TEST(Api3dRepo, VolumeInfoUnknownIdReturnsFalse) {
|
||||||
|
StubAsyncRepo dsRepo;
|
||||||
|
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(22.0, 114.0);
|
||||||
|
Api3dRepository repo(dsRepo, frame);
|
||||||
|
|
||||||
|
Api3dRepository::VolumeInfo info;
|
||||||
|
EXPECT_FALSE(repo.volumeInfo("not-a-volume", info));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue