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
5 changed files with 71 additions and 41 deletions
Showing only changes of commit 572fbf8d7b - Show all commits

View File

@ -486,30 +486,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const auto slices = scene3dRepo->sliceRows(); const auto slices = scene3dRepo->sliceRows();
const auto anomalies = scene3dRepo->anomalyRows(); const auto anomalies = scene3dRepo->anomalyRows();
// 电阻率/视/瞬变段=对象树反演 dssplitByCategory 按 dsTypeCode 分voxel 段下方单独喂完整树)。 // 电阻率/视/瞬变段=对象树反演 dssplitByCategory 按 dsTypeCode 分voxel 段下方单独喂完整树)。
// 各段据结构建「从项目根的容器层级树」(项目根/GS/TM → dsCategorySection 自剪枝)。
drawer->analysisTab()->setStructure(*lastStructNodes);
drawer->analysisTab()->setBuckets(geopro::app::splitByCategory(*lastSourceRows)); drawer->analysisTab()->setBuckets(geopro::app::splitByCategory(*lastSourceRows));
// 三维体段=从项目根的完整层级树:项目根/GS/TM 容器节点 → 体(挂生成位置) → 切片(挂体) → 异常(挂体/切片)。 // 三维体段=体 + 切片 + 异常;容器层级由 CategorySection 据 structure + 各 ds 的 structParentId 自建。
// populateDatasetList 按 parentId 自动建树spec §7/§8
std::vector<geopro::data::DsRow> voxelTree; std::vector<geopro::data::DsRow> voxelTree;
// 剪枝:只展示有三维体挂载的容器路径(从体的生成位置向上追溯到根),不带出无关 GS/TM。
std::map<std::string, const geopro::data::StructNode*> byId;
for (const auto& n : *lastStructNodes) byId[n.id] = &n;
std::set<std::string> keep;
for (const auto& v : vols) {
std::string p = v.parentId; // 体的生成位置(structParentId)
while (!p.empty() && byId.count(p) && !keep.count(p)) {
keep.insert(p);
p = byId[p]->parentId; // 向上到祖先GS/项目根)
}
}
for (const auto& n : *lastStructNodes) { // 仅保留有体路径的容器节点ddCode="container",无复选框)
if (!keep.count(n.id)) continue;
geopro::data::DsRow c;
c.id = n.id;
c.dsName = n.name;
c.ddCode = "container";
c.parentId = n.parentId;
voxelTree.push_back(std::move(c));
}
for (const auto& v : vols) voxelTree.push_back(v); for (const auto& v : vols) voxelTree.push_back(v);
for (const auto& s : slices) voxelTree.push_back(s); for (const auto& s : slices) voxelTree.push_back(s);
for (const auto& a : anomalies) voxelTree.push_back(a); for (const auto& a : anomalies) voxelTree.push_back(a);

View File

@ -39,7 +39,10 @@ public:
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override { QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override {
const bool special = const bool special =
idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable); idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable);
return QSize(0, special ? 34 : 52); if (special) return QSize(0, 34);
// 无副标题(容器节点 项目/GS/TM用紧凑矮卡避免标题下大片留白。
const bool hasMeta = idx.data(Qt::DisplayRole).toString().contains(QLatin1Char('\n'));
return QSize(0, hasMeta ? 52 : 30);
} }
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override { void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override {
@ -81,8 +84,8 @@ public:
geopro::app::tokenColor("accent/primary")); geopro::app::tokenColor("accent/primary"));
} }
// 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。 // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。无复选框项(容器节点)左留白小。
int textLeftPad = 14; int textLeftPad = 6;
const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable); const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable);
if (checkable) { if (checkable) {
const int box = 16; const int box = 16;
@ -105,17 +108,19 @@ public:
meta = disp.mid(nl + 1); meta = disp.mid(nl + 1);
} }
const QRect textR = r.adjusted(textLeftPad, 6, -12, -6); const QRect textR = r.adjusted(textLeftPad, 4, -12, -4);
// 标题
QFont tf = opt.font; QFont tf = opt.font;
tf.setPixelSize(geopro::app::scaledPx(13)); tf.setPixelSize(geopro::app::scaledPx(13));
p->setFont(tf); p->setFont(tf);
p->setPen(geopro::app::tokenColor("text/primary")); p->setPen(geopro::app::tokenColor("text/primary"));
const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2); if (meta.isEmpty()) {
p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter, // 无副标题(容器节点):标题垂直居中整卡,不留下半空白。
p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width())); p->drawText(textR, Qt::AlignLeft | Qt::AlignVCenter,
// 元信息 p->fontMetrics().elidedText(title, Qt::ElideRight, textR.width()));
if (!meta.isEmpty()) { } else {
const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2);
p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width()));
QFont mf = opt.font; QFont mf = opt.font;
mf.setPixelSize(geopro::app::scaledPx(11)); mf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(mf); p->setFont(mf);

View File

@ -2,6 +2,8 @@
#include <QComboBox> #include <QComboBox>
#include <QDate> #include <QDate>
#include <map>
#include <set>
#include "panels/columns/DateRangeEdit.hpp" #include "panels/columns/DateRangeEdit.hpp"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
@ -61,8 +63,11 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd); body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd);
body->setSpacing(space::kSm); body->setSpacing(space::kSm);
// 筛选行:装置类型(仅 ERT 类+ 采集时间段(起止;最小日期显示"不限",不再露 1752)。 // 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。
auto* filterRow = new QHBoxLayout(); auto* filterRow = new QHBoxLayout();
dateRange_ = new DateRangeEdit(body_);
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
filterRow->addWidget(dateRange_, 1);
if (spec_.hasArrayTypeFilter) { if (spec_.hasArrayTypeFilter) {
arrayCombo_ = new QComboBox(body_); arrayCombo_ = new QComboBox(body_);
arrayCombo_->addItem(QStringLiteral("全部装置"), QString()); arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
@ -70,16 +75,13 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset
[this](int) { rebuildList(); }); [this](int) { rebuildList(); });
filterRow->addWidget(arrayCombo_); filterRow->addWidget(arrayCombo_);
} }
filterRow->addWidget(new QLabel(QStringLiteral("采集"), body_));
dateRange_ = new DateRangeEdit(body_);
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
filterRow->addWidget(dateRange_, 1);
body->addLayout(filterRow); body->addLayout(filterRow);
// 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。 // 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。
list_ = new QTreeWidget(body_); list_ = new QTreeWidget(body_);
list_->setHeaderHidden(true); list_->setHeaderHidden(true);
list_->setRootIsDecorated(true); list_->setRootIsDecorated(true);
list_->setIndentation(14); // 紧凑父子缩进(默认 20 太宽)
applyDatasetCardDelegate(list_); applyDatasetCardDelegate(list_);
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) { emitChecked(); }); connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) { emitChecked(); });
connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) { connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) {
@ -168,9 +170,39 @@ void CategorySection::rebuildList() {
filtered.reserve(rows_.size()); filtered.reserve(rows_.size());
for (const auto& r : rows_) for (const auto& r : rows_)
if (passesFilters(r)) filtered.push_back(r); if (passesFilters(r)) filtered.push_back(r);
// 从项目根的层级树:容器节点(结构,剪枝仅留有 ds 的路径)+ ds挂 parentId 或 structParentId
std::vector<DsRow> display;
if (!structure_.empty()) {
std::map<std::string, const geopro::data::StructNode*> byId;
for (const auto& n : structure_) byId[n.id] = &n;
std::set<std::string> keep; // 收集每个 ds 的结构归属向上的祖先链
for (const auto& d : filtered) {
std::string p = d.structParentId;
while (!p.empty() && byId.count(p) && !keep.count(p)) {
keep.insert(p);
p = byId[p]->parentId;
}
}
for (const auto& n : structure_)
if (keep.count(n.id)) {
DsRow c;
c.id = n.id;
c.dsName = n.name;
c.ddCode = "container";
c.parentId = n.parentId;
display.push_back(std::move(c));
}
}
for (const auto& d : filtered) {
DsRow x = d;
if (x.parentId.empty()) x.parentId = x.structParentId; // 源 ds / 体 → 挂结构容器
display.push_back(std::move(x));
}
{ {
const QSignalBlocker block(list_); const QSignalBlocker block(list_);
populateDatasetList(list_, filtered, /*append=*/false); populateDatasetList(list_, display, /*append=*/false);
for (QTreeWidgetItemIterator it(list_); *it; ++it) { for (QTreeWidgetItemIterator it(list_); *it; ++it) {
// 容器节点(项目根/GS/TM只作层级骨架——明确去掉复选框、不可勾选。 // 容器节点(项目根/GS/TM只作层级骨架——明确去掉复选框、不可勾选。
if ((*it)->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) { if ((*it)->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) {

View File

@ -1,5 +1,6 @@
#include "api/Api3dRepository.hpp" #include "api/Api3dRepository.hpp"
#include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QJsonDocument> #include <QJsonDocument>
#include <QObject> #include <QObject>
@ -103,7 +104,12 @@ bool Api3dRepository::isVolumeDataset(const std::string& dsId) const {
std::string Api3dRepository::createVolume(VolumeBuildParams params, const std::string& name) { std::string Api3dRepository::createVolume(VolumeBuildParams params, const std::string& name) {
const std::string id = "vol-" + std::to_string(++volumeCounter_); const std::string id = "vol-" + std::to_string(++volumeCounter_);
volumes_[id] = StoredVolume{std::move(params), name, std::nullopt}; StoredVolume sv;
sv.params = std::move(params);
sv.name = name;
sv.createTime =
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString();
volumes_[id] = std::move(sv);
return id; return id;
} }
@ -129,7 +135,8 @@ std::vector<DsRow> Api3dRepository::volumeRows() const {
r.dsName = sv.name; r.dsName = sv.name;
r.ddCode = "dd_voxel"; r.ddCode = "dd_voxel";
r.typeName = "三维体"; r.typeName = "三维体";
r.parentId = sv.request ? sv.request->structParentId : std::string(); // 挂生成位置(项目根/GS/TM) r.structParentId = sv.request ? sv.request->structParentId : std::string(); // 结构归属(生成位置)
r.createTime = sv.createTime;
rows.push_back(std::move(r)); rows.push_back(std::move(r));
} }
return rows; return rows;
@ -313,6 +320,7 @@ std::vector<DsRow> Api3dRepository::sliceRows() const {
r.ddCode = "dd_slice"; r.ddCode = "dd_slice";
r.typeName = "切片"; r.typeName = "切片";
r.parentId = ss.spec.volumeDsId; // 树中挂在所属三维体下 r.parentId = ss.spec.volumeDsId; // 树中挂在所属三维体下
r.createTime = ss.createTime;
rows.push_back(std::move(r)); rows.push_back(std::move(r));
} }
return rows; return rows;
@ -343,7 +351,9 @@ bool Api3dRepository::sliceSpec(const std::string& dsId, SliceSpec& out) const {
void Api3dRepository::createSlice(const SliceSpec& spec, const std::string& name, void Api3dRepository::createSlice(const SliceSpec& spec, const std::string& name,
std::function<void(std::string)> onOk, OnError /*onErr*/) { std::function<void(std::string)> onOk, OnError /*onErr*/) {
const std::string id = "slice-" + std::to_string(++sliceCounter_); const std::string id = "slice-" + std::to_string(++sliceCounter_);
slices_[id] = StoredSlice{spec, name}; slices_[id] = StoredSlice{
spec, name,
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString()};
onOk(id); onOk(id);
} }

View File

@ -129,6 +129,7 @@ private:
core::ColorScale cachedScale; // 与 cachedGrid 同时填(源剖面色阶) core::ColorScale cachedScale; // 与 cachedGrid 同时填(源剖面色阶)
std::optional<std::size_t> pointCount; // 聚合散点数finalizeVolume 时持久化,详情统计用) std::optional<std::size_t> pointCount; // 聚合散点数finalizeVolume 时持久化,详情统计用)
std::optional<VoxelGenerateRequest> request; // 组装的真实请求体createVolume(req) 路径填充) std::optional<VoxelGenerateRequest> request; // 组装的真实请求体createVolume(req) 路径填充)
std::string createTime; // 创建时刻mock列表副标题/详情用)
}; };
std::map<std::string, StoredVolume> volumes_; // dsId → 体 std::map<std::string, StoredVolume> volumes_; // dsId → 体
int volumeCounter_ = 0; int volumeCounter_ = 0;
@ -137,6 +138,7 @@ private:
struct StoredSlice { struct StoredSlice {
SliceSpec spec; SliceSpec spec;
std::string name; std::string name;
std::string createTime; // 创建时刻
}; };
std::map<std::string, StoredSlice> slices_; // dsId → 切片 std::map<std::string, StoredSlice> slices_; // dsId → 切片
int sliceCounter_ = 0; int sliceCounter_ = 0;