#include "panels/columns/Column3DAnalysis.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #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(&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& 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& rows) { // 按 dsId 保留刷新前的勾选态:列表重建(保存切片/生成体追加一行也会整树重建)不应丢已勾选项 // 的渲染态——否则保存切片会连带取消三维体勾选、把它从场景移除(实测 bug)。 // 切换测线(新数据)时旧 id 不匹配 → 自然全空,行为与原先一致。 QSet 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 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