198 lines
9.6 KiB
C++
198 lines
9.6 KiB
C++
#include "panels/columns/Column3DAnalysis.hpp"
|
||
|
||
#include <QComboBox>
|
||
#include <QGroupBox>
|
||
#include <QHBoxLayout>
|
||
#include <QLabel>
|
||
#include <QMenu>
|
||
#include <QSet>
|
||
#include <QSignalBlocker>
|
||
#include <QSplitter>
|
||
#include <QTreeWidget>
|
||
#include <QTreeWidgetItem>
|
||
#include <QTreeWidgetItemIterator>
|
||
#include <QVBoxLayout>
|
||
|
||
#include "Theme.hpp"
|
||
#include "panels/DatasetListPanel.hpp"
|
||
|
||
namespace geopro::app {
|
||
|
||
Column3DAnalysis::Column3DAnalysis(QWidget* parent) : QWidget(parent) {
|
||
auto* root = new QVBoxLayout(this);
|
||
root->setContentsMargins(space::kMd, space::kMd, space::kMd, space::kMd);
|
||
root->setSpacing(space::kMd);
|
||
|
||
tree_ = new QTreeWidget();
|
||
tree_->setHeaderHidden(true);
|
||
tree_->setRootIsDecorated(true);
|
||
applyDatasetCardDelegate(tree_);
|
||
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||
|
||
connect(tree_, &QTreeWidget::customContextMenuRequested, this, &Column3DAnalysis::onContextMenu);
|
||
|
||
connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
||
QStringList ids;
|
||
for (QTreeWidgetItemIterator it(tree_); *it; ++it) {
|
||
if ((*it)->checkState(0) == Qt::Checked)
|
||
ids << (*it)->data(0, kDsIdRole).toString();
|
||
}
|
||
emit checkedItemsChanged(ids);
|
||
});
|
||
|
||
// ── 数据集树(上) + 「异常」分组(下) 放进竖向 Splitter:可拖拽、清晰分隔,数据集树占多数 ──
|
||
// ── 3D 异常控制(#4c):分组框内含 显示过滤下拉 + 异常列表(每条显隐勾选;选中联动 VTK)──
|
||
anomalyTree_ = new QTreeWidget();
|
||
anomalyTree_->setHeaderHidden(true);
|
||
anomalyTree_->setRootIsDecorated(false);
|
||
anomalyTree_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||
connect(anomalyTree_, &QTreeWidget::customContextMenuRequested, this,
|
||
&Column3DAnalysis::onAnomalyContextMenu);
|
||
connect(anomalyTree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* it, int) {
|
||
if (it == nullptr) return;
|
||
emit anomalyVisibilityChanged(it->data(0, kDsIdRole).toString(),
|
||
it->checkState(0) == Qt::Checked);
|
||
});
|
||
connect(anomalyTree_, &QTreeWidget::currentItemChanged, this,
|
||
[this](QTreeWidgetItem* cur, QTreeWidgetItem*) {
|
||
if (cur != nullptr) emit anomalySelected(cur->data(0, kDsIdRole).toString());
|
||
});
|
||
|
||
auto* anomGroup = new QGroupBox(QStringLiteral("异常"));
|
||
auto* gv = new QVBoxLayout(anomGroup);
|
||
gv->setContentsMargins(space::kSm, space::kSm, space::kSm, space::kSm);
|
||
gv->setSpacing(space::kSm);
|
||
{
|
||
auto* fr = new QHBoxLayout();
|
||
fr->addWidget(new QLabel(QStringLiteral("显示")));
|
||
anomalyFilter_ = new QComboBox();
|
||
anomalyFilter_->addItem(QStringLiteral("全部显示")); // 0
|
||
anomalyFilter_->addItem(QStringLiteral("随GS")); // 1
|
||
anomalyFilter_->addItem(QStringLiteral("随数据集")); // 2
|
||
anomalyFilter_->addItem(QStringLiteral("全部隐藏")); // 3
|
||
anomalyFilter_->setCurrentIndex(2); // 默认随数据集(= 跟当前三维体显隐)
|
||
connect(anomalyFilter_, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||
[this](int idx) { emit anomalyDisplayFilterChanged(idx); });
|
||
fr->addWidget(anomalyFilter_, 1);
|
||
gv->addLayout(fr);
|
||
}
|
||
gv->addWidget(anomalyTree_, 1);
|
||
|
||
auto* splitter = new QSplitter(Qt::Vertical);
|
||
splitter->setChildrenCollapsible(false);
|
||
splitter->addWidget(tree_);
|
||
splitter->addWidget(anomGroup);
|
||
splitter->setStretchFactor(0, 3); // 数据集树占多
|
||
splitter->setStretchFactor(1, 2);
|
||
root->addWidget(splitter, 1);
|
||
}
|
||
|
||
int Column3DAnalysis::anomalyFilterMode() const {
|
||
return anomalyFilter_ ? anomalyFilter_->currentIndex() : 2;
|
||
}
|
||
|
||
void Column3DAnalysis::setAnomalies(const std::vector<geopro::core::Anomaly>& anoms) {
|
||
QSignalBlocker block(anomalyTree_); // 填充不触发 visibilityChanged
|
||
anomalyTree_->clear();
|
||
for (const auto& a : anoms) {
|
||
auto* item = new QTreeWidgetItem(anomalyTree_);
|
||
const QString name = a.name.empty() ? QStringLiteral("异常") : QString::fromStdString(a.name);
|
||
const QString type = a.typeName.empty() ? QString() : QString::fromStdString(a.typeName);
|
||
item->setText(0, type.isEmpty() ? name : QStringLiteral("%1(%2)").arg(name, type));
|
||
item->setData(0, kDsIdRole, QString::fromStdString(a.id));
|
||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||
item->setCheckState(0, Qt::Checked); // 默认显示
|
||
}
|
||
}
|
||
|
||
void Column3DAnalysis::onAnomalyContextMenu(const QPoint& pos) {
|
||
QTreeWidgetItem* it = anomalyTree_->itemAt(pos);
|
||
if (it == nullptr) return;
|
||
const QString id = it->data(0, kDsIdRole).toString();
|
||
QMenu menu(this);
|
||
menu.addAction(QStringLiteral("删除异常"), this, [this, id] { emit anomalyDeleteRequested(id); });
|
||
menu.exec(anomalyTree_->viewport()->mapToGlobal(pos));
|
||
}
|
||
|
||
void Column3DAnalysis::setDatasets(const std::vector<geopro::data::DsRow>& rows) {
|
||
// 按 dsId 保留刷新前的勾选态:列表重建(保存切片/生成体追加一行也会整树重建)不应丢已勾选项
|
||
// 的渲染态——否则保存切片会连带取消三维体勾选、把它从场景移除(实测 bug)。
|
||
// 切换测线(新数据)时旧 id 不匹配 → 自然全空,行为与原先一致。
|
||
QSet<QString> wasChecked;
|
||
for (QTreeWidgetItemIterator it(tree_); *it; ++it)
|
||
if ((*it)->checkState(0) == Qt::Checked)
|
||
wasChecked.insert((*it)->data(0, kDsIdRole).toString());
|
||
|
||
{
|
||
QSignalBlocker blocker(tree_);
|
||
populateDatasetList(tree_, rows, /*append=*/false);
|
||
for (QTreeWidgetItemIterator it(tree_); *it; ++it) {
|
||
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
|
||
const QString id = (*it)->data(0, kDsIdRole).toString();
|
||
(*it)->setCheckState(0, wasChecked.contains(id) ? Qt::Checked : Qt::Unchecked);
|
||
}
|
||
} // blocker released here
|
||
// 仅当勾选集真正变化才发信号:重建但勾选集不变(如保存切片仅追加一行)→ 不发,
|
||
// 避免下游 syncSlices 用"尚未勾选新切片"的中间态误隐藏刚链接的切片(闪烁/重复)。
|
||
QStringList ids;
|
||
QSet<QString> nowChecked;
|
||
for (QTreeWidgetItemIterator it(tree_); *it; ++it)
|
||
if ((*it)->checkState(0) == Qt::Checked) {
|
||
const QString id = (*it)->data(0, kDsIdRole).toString();
|
||
ids << id;
|
||
nowChecked.insert(id);
|
||
}
|
||
if (nowChecked != wasChecked) emit checkedItemsChanged(ids);
|
||
}
|
||
|
||
void Column3DAnalysis::setItemChecked(const QString& dsId, bool checked) {
|
||
for (QTreeWidgetItemIterator it(tree_); *it; ++it) {
|
||
if ((*it)->data(0, kDsIdRole).toString() != dsId) continue;
|
||
for (QTreeWidgetItem* p = (*it)->parent(); p != nullptr; p = p->parent())
|
||
p->setExpanded(true); // 展开父链 → 新勾选行可见
|
||
// setCheckState 仅在状态变化时发 itemChanged → checkedItemsChanged(驱动渲染同步)。
|
||
(*it)->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked);
|
||
return;
|
||
}
|
||
}
|
||
|
||
void Column3DAnalysis::onContextMenu(const QPoint& pos) {
|
||
QTreeWidgetItem* it = tree_->itemAt(pos);
|
||
if (!it) return;
|
||
|
||
const QString dsId = it->data(0, kDsIdRole).toString();
|
||
const QString ddCode = it->data(0, kDsDdCodeRole).toString();
|
||
const QString name = it->data(0, kDsNameRole).toString();
|
||
const bool isSlice = (ddCode == QStringLiteral("dd_slice"));
|
||
|
||
QMenu menu(this);
|
||
if (!isSlice) {
|
||
// 三维体数据集:切片▸(上下/前后/左右/任意) / 色阶 / 数据详情。
|
||
// 显示/隐藏 = 勾选框,故菜单不再重复提供(去冗余)。
|
||
QMenu* sub = menu.addMenu(QStringLiteral("切片"));
|
||
using SA = geopro::render::interact::SliceAxis;
|
||
sub->addAction(QStringLiteral("上下"), this, [this]{ emit sliceRequested(SA::UpDown); });
|
||
sub->addAction(QStringLiteral("前后"), this, [this]{ emit sliceRequested(SA::FrontBack); });
|
||
sub->addAction(QStringLiteral("左右"), this, [this]{ emit sliceRequested(SA::LeftRight); });
|
||
sub->addAction(QStringLiteral("任意"), this, [this]{ emit sliceRequested(SA::Oblique); });
|
||
menu.addAction(QStringLiteral("色阶"), this, [this, dsId]{ emit colorScaleRequested(dsId); });
|
||
menu.addAction(QStringLiteral("数据详情"), this, [this, dsId, ddCode, name]{ emit detailRequested(dsId, ddCode, name); });
|
||
} else {
|
||
// 切片数据集:保存(覆盖位姿) / 保存为(另存新切片) / 导出▸(图片·dat) / 删除 / 色阶 / 数据详情。
|
||
// 显示/隐藏 = 勾选框,去冗余。导出与 VTK 视图切片右键统一为二级菜单。
|
||
menu.addAction(QStringLiteral("保存"), this, [this, dsId]{ emit sliceSaveRequested(dsId); });
|
||
menu.addAction(QStringLiteral("保存为"), this, [this, dsId]{ emit sliceSaveAsRequested(dsId); });
|
||
QMenu* exp = menu.addMenu(QStringLiteral("导出"));
|
||
exp->addAction(QStringLiteral("图片"), this, [this, dsId]{ emit sliceExportImageRequested(dsId); });
|
||
exp->addAction(QStringLiteral("dat"), this, [this, dsId]{ emit sliceExportDatRequested(dsId); });
|
||
menu.addAction(QStringLiteral("删除"), this, [this, dsId]{ emit sliceDeleteRequested(dsId); });
|
||
menu.addSeparator();
|
||
menu.addAction(QStringLiteral("色阶"), this, [this, dsId]{ emit colorScaleRequested(dsId); });
|
||
menu.addAction(QStringLiteral("数据详情"), this, [this, dsId, ddCode, name]{ emit detailRequested(dsId, ddCode, name); });
|
||
}
|
||
|
||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||
}
|
||
|
||
} // namespace geopro::app
|