diff --git a/src/app/main.cpp b/src/app/main.cpp index c1ed132..fdd6107 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -386,31 +386,51 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } }; - // 当前活动体的异常重载渲染(#4b):体到场→载其异常 actor;体移除→清空。持久化跨重勾可见。 - auto reloadAnomalies = [sceneView, scene3dRepo]() { + // 异常刷新渲染 + 填充三维分析栏异常列表(#4b/4c):按显示过滤档位决定异常集合。 + // 0 全部显示=所有异常;1 随GS/2 随数据集=当前活动体的异常;3 全部隐藏=不渲染、列表空。 + // (随GS 暂同随数据集,无 GS 分组数据。loadAnomalyTree 空 key→全部,非空→该体。mock 同步回调。) + auto refreshAnomalies = [sceneView, scene3dRepo, drawer, renderWindowPtr]() { sceneView->clearAnomalies(); - const std::string vol = sceneView->currentVolumeDsId(); - if (vol.empty()) return; + auto* ca = drawer->colAnalysis(); + const int mode = ca->anomalyFilterMode(); + if (mode == 3) { // 全部隐藏 + ca->setAnomalies({}); + renderWindowPtr->Render(); + return; + } + std::string key; // 空 = 全部 + if (mode != 0) { // 随GS/随数据集 → 当前活动体 + key = sceneView->currentVolumeDsId(); + if (key.empty()) { // 无活动体 → 空 + ca->setAnomalies({}); + renderWindowPtr->Render(); + return; + } + } + std::vector set; scene3dRepo->loadAnomalyTree( - vol, - [sceneView](geopro::data::I3dSceneRepository::AnomalyTree tree) { + key, + [&set](geopro::data::I3dSceneRepository::AnomalyTree tree) { for (auto& b : tree.bodies) - for (auto& a : b.members) sceneView->addAnomaly(a); - for (auto& a : tree.loose) sceneView->addAnomaly(a); + for (auto& a : b.members) set.push_back(a); + for (auto& a : tree.loose) set.push_back(a); }, [](const std::string&) {}); + for (const auto& a : set) sceneView->addAnomaly(a); + ca->setAnomalies(set); // 填充列表(每条显隐勾选默认显示) + renderWindowPtr->Render(); // 必须重绘:clear+addAnomaly 改了 prop,否则 VTK 不刷新(与列表脱节) }; // 体素变化(重建/清场)后把体素 image 推给 InteractionManager(切片基底),并调和已保存切片 + 异常。 - sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, reloadAnomalies]() { + sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, refreshAnomalies]() { if (sceneView->hasVolume()) interactionMgr->setVolumeImage(sceneView->currentVolumeImage(), sceneView->currentColorScale(), sceneView->currentVmin(), sceneView->currentVmax()); else interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0); - syncSlices(); // 体到场/移除后重建当前体下已勾选的切片 - reloadAnomalies(); // 同步重载当前体的异常 actor + syncSlices(); // 体到场/移除后重建当前体下已勾选的切片 + refreshAnomalies(); // 同步重载异常 actor + 刷新异常列表 }; // ── 三栏抽屉信号 → 控制器/交互(Task 7 接线)────────────────────────────── @@ -442,8 +462,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿);导出统一为「导出▸图片·dat」; // 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。 interactionMgr->onSliceContextMenuRequested = - [&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, drawer, anomalyDrawTool, - renderWindowPtr]() { + [&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, refreshAnomalies, drawer, + anomalyDrawTool, renderWindowPtr]() { QMenu menu(&window); QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常")); QAction* aSave = menu.addAction(QStringLiteral("保存")); @@ -472,7 +492,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const std::string volId = sceneView->currentVolumeDsId(); anomalyDrawTool->start( o, normal, - [&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnalysis, volId, + [&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies, volId, normal, o](const std::vector& worldPts) { // 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。 geopro::core::Anomaly a; @@ -504,16 +524,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re a.typeName = dlg.typeName().toStdString(); a.exceptionTypeId = dlg.typeId().toStdString(); a.remark = dlg.remark().toStdString(); - geopro::core::Anomaly finalA = a; scene3dRepo->saveAnomaly( - finalA, shot.toStdString(), - [sceneView, renderWindowPtr, refreshAnalysis, draftId, - finalA](std::string id) mutable { - sceneView->removeAnomaly(draftId); // 撤草稿,换真实 id 重渲染 - finalA.id = id; - sceneView->addAnomaly(finalA); + a, shot.toStdString(), + [sceneView, renderWindowPtr, refreshAnomalies, draftId](std::string) { + sceneView->removeAnomaly(draftId); // 撤草稿 + refreshAnomalies(); // 重渲染 + 刷新异常列表(含新异常) renderWindowPtr->Render(); - refreshAnalysis(); // 列表刷新(4c 异常面板接入后体现) }, [&window](const std::string& m) { QMessageBox::warning(&window, QStringLiteral("保存异常"), @@ -754,6 +770,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re QStringLiteral("色阶设置开发中。")); }); + // ── 3D 异常控制(#4c):显示过滤 / 单条显隐 / 删除 → 驱动 VTK 异常渲染 ────────── + // 过滤档位变化 → 重算异常集合并重渲染 + 刷新列表(独立于体勾选)。 + QObject::connect(ca, &geopro::app::Column3DAnalysis::anomalyDisplayFilterChanged, vtkWidget, + [refreshAnomalies](int) { refreshAnomalies(); }); + // 单条显隐 → 切该异常 actor 可见性。 + QObject::connect(ca, &geopro::app::Column3DAnalysis::anomalyVisibilityChanged, vtkWidget, + [sceneView, renderWindowPtr](const QString& id, bool vis) { + sceneView->setAnomalyVisible(id.toStdString(), vis); + renderWindowPtr->Render(); + }); + // 删除异常 → 删 mock + 刷新渲染/列表。 + QObject::connect(ca, &geopro::app::Column3DAnalysis::anomalyDeleteRequested, &window, + [scene3dRepo, refreshAnomalies](const QString& id) { + scene3dRepo->deleteAnomaly( + id.toStdString(), [refreshAnomalies]() { refreshAnomalies(); }, + [](const std::string&) {}); + }); + // ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token,经同一共享 GeoLocalFrame 配准)── auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window); // 当前底图选择(默认 天地图=Satellite,对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。 diff --git a/src/app/panels/columns/Column3DAnalysis.cpp b/src/app/panels/columns/Column3DAnalysis.cpp index a99f834..01720ad 100644 --- a/src/app/panels/columns/Column3DAnalysis.cpp +++ b/src/app/panels/columns/Column3DAnalysis.cpp @@ -1,8 +1,13 @@ #include "panels/columns/Column3DAnalysis.hpp" +#include +#include +#include +#include #include #include #include +#include #include #include #include @@ -35,7 +40,78 @@ Column3DAnalysis::Column3DAnalysis(QWidget* parent) : QWidget(parent) { emit checkedItemsChanged(ids); }); - root->addWidget(tree_, 1); + // ── 数据集树(上) + 「异常」分组(下) 放进竖向 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) { diff --git a/src/app/panels/columns/Column3DAnalysis.hpp b/src/app/panels/columns/Column3DAnalysis.hpp index d6a88db..80285b7 100644 --- a/src/app/panels/columns/Column3DAnalysis.hpp +++ b/src/app/panels/columns/Column3DAnalysis.hpp @@ -3,14 +3,16 @@ #include #include #include "repo/RepoTypes.hpp" +#include "model/Anomaly.hpp" #include "interact/SlicePlaneMath.hpp" // SliceAxis class QTreeWidget; +class QComboBox; class QPoint; namespace geopro::app { -// 三维分析栏:对象→三维体模型→切片 树 + 三维体/切片两套右键菜单。 +// 三维分析栏:对象→三维体模型→切片 树 + 三维体/切片两套右键菜单 + 3D 异常控制(列表/过滤/显隐)。 class Column3DAnalysis : public QWidget { Q_OBJECT public: @@ -19,6 +21,10 @@ public: void setDatasets(const std::vector& rows); // Analysis 维度(三维体/切片) // 程序化勾选某 dsId 的行(保存切片后自动勾选新行)+ 展开其父节点使可见。 void setItemChecked(const QString& dsId, bool checked); + // 3D 异常列表(#4c):每条带显隐勾选;选中联动 VTK。anoms 为当前应展示的异常集合。 + void setAnomalies(const std::vector& anoms); + // 当前显示过滤档位(0全部显示/1随GS/2随数据集/3全部隐藏)。 + int anomalyFilterMode() const; signals: void sliceRequested(geopro::render::interact::SliceAxis axis); // 三维体右键 切片▸(上下/前后/左右/任意) @@ -30,10 +36,18 @@ signals: void sliceExportDatRequested(const QString& dsId); // 导出▸dat void sliceDeleteRequested(const QString& dsId); void checkedItemsChanged(const QStringList& dsIds); + // ── 异常(#4c)── + void anomalyVisibilityChanged(const QString& anomalyId, bool visible); // 单条显隐勾选 + void anomalyDisplayFilterChanged(int mode); // 过滤档位 0..3 + void anomalySelected(const QString& anomalyId); // 列表选中→VTK 高亮 + void anomalyDeleteRequested(const QString& anomalyId); // 右键删除 private: void onContextMenu(const QPoint& pos); + void onAnomalyContextMenu(const QPoint& pos); QTreeWidget* tree_ = nullptr; + QTreeWidget* anomalyTree_ = nullptr; + QComboBox* anomalyFilter_ = nullptr; }; } // namespace geopro::app