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
3 changed files with 148 additions and 24 deletions
Showing only changes of commit 324d4ac605 - Show all commits

View File

@ -386,23 +386,43 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
} }
}; };
// 当前活动体的异常重载渲染(#4b体到场→载其异常 actor体移除→清空。持久化跨重勾可见。 // 异常刷新渲染 + 填充三维分析栏异常列表(#4b/4c按显示过滤档位决定异常集合。
auto reloadAnomalies = [sceneView, scene3dRepo]() { // 0 全部显示=所有异常1 随GS/2 随数据集=当前活动体的异常3 全部隐藏=不渲染、列表空。
// 随GS 暂同随数据集,无 GS 分组数据。loadAnomalyTree 空 key→全部非空→该体。mock 同步回调。)
auto refreshAnomalies = [sceneView, scene3dRepo, drawer, renderWindowPtr]() {
sceneView->clearAnomalies(); sceneView->clearAnomalies();
const std::string vol = sceneView->currentVolumeDsId(); auto* ca = drawer->colAnalysis();
if (vol.empty()) return; 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<geopro::core::Anomaly> set;
scene3dRepo->loadAnomalyTree( scene3dRepo->loadAnomalyTree(
vol, key,
[sceneView](geopro::data::I3dSceneRepository::AnomalyTree tree) { [&set](geopro::data::I3dSceneRepository::AnomalyTree tree) {
for (auto& b : tree.bodies) for (auto& b : tree.bodies)
for (auto& a : b.members) sceneView->addAnomaly(a); for (auto& a : b.members) set.push_back(a);
for (auto& a : tree.loose) sceneView->addAnomaly(a); for (auto& a : tree.loose) set.push_back(a);
}, },
[](const std::string&) {}); [](const std::string&) {});
for (const auto& a : set) sceneView->addAnomaly(a);
ca->setAnomalies(set); // 填充列表(每条显隐勾选默认显示)
renderWindowPtr->Render(); // 必须重绘clear+addAnomaly 改了 prop否则 VTK 不刷新(与列表脱节)
}; };
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager切片基底并调和已保存切片 + 异常。 // 体素变化(重建/清场)后把体素 image 推给 InteractionManager切片基底并调和已保存切片 + 异常。
sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, reloadAnomalies]() { sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, refreshAnomalies]() {
if (sceneView->hasVolume()) if (sceneView->hasVolume())
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(), interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
sceneView->currentColorScale(), sceneView->currentVmin(), sceneView->currentColorScale(), sceneView->currentVmin(),
@ -410,7 +430,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
else else
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0); interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
syncSlices(); // 体到场/移除后重建当前体下已勾选的切片 syncSlices(); // 体到场/移除后重建当前体下已勾选的切片
reloadAnomalies(); // 同步重载当前体的异常 actor refreshAnomalies(); // 同步重载异常 actor + 刷新异常列表
}; };
// ── 三栏抽屉信号 → 控制器/交互Task 7 接线)────────────────────────────── // ── 三栏抽屉信号 → 控制器/交互Task 7 接线)──────────────────────────────
@ -442,8 +462,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿)导出统一为「导出▸图片·dat」 // 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿)导出统一为「导出▸图片·dat」
// 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。 // 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。
interactionMgr->onSliceContextMenuRequested = interactionMgr->onSliceContextMenuRequested =
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, drawer, anomalyDrawTool, [&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, refreshAnomalies, drawer,
renderWindowPtr]() { anomalyDrawTool, renderWindowPtr]() {
QMenu menu(&window); QMenu menu(&window);
QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常")); QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常"));
QAction* aSave = 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(); const std::string volId = sceneView->currentVolumeDsId();
anomalyDrawTool->start( anomalyDrawTool->start(
o, normal, o, normal,
[&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnalysis, volId, [&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies, volId,
normal, o](const std::vector<ri::Vec3>& worldPts) { normal, o](const std::vector<ri::Vec3>& worldPts) {
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。 // 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
geopro::core::Anomaly a; geopro::core::Anomaly a;
@ -504,16 +524,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
a.typeName = dlg.typeName().toStdString(); a.typeName = dlg.typeName().toStdString();
a.exceptionTypeId = dlg.typeId().toStdString(); a.exceptionTypeId = dlg.typeId().toStdString();
a.remark = dlg.remark().toStdString(); a.remark = dlg.remark().toStdString();
geopro::core::Anomaly finalA = a;
scene3dRepo->saveAnomaly( scene3dRepo->saveAnomaly(
finalA, shot.toStdString(), a, shot.toStdString(),
[sceneView, renderWindowPtr, refreshAnalysis, draftId, [sceneView, renderWindowPtr, refreshAnomalies, draftId](std::string) {
finalA](std::string id) mutable { sceneView->removeAnomaly(draftId); // 撤草稿
sceneView->removeAnomaly(draftId); // 撤草稿,换真实 id 重渲染 refreshAnomalies(); // 重渲染 + 刷新异常列表(含新异常)
finalA.id = id;
sceneView->addAnomaly(finalA);
renderWindowPtr->Render(); renderWindowPtr->Render();
refreshAnalysis(); // 列表刷新4c 异常面板接入后体现)
}, },
[&window](const std::string& m) { [&window](const std::string& m) {
QMessageBox::warning(&window, QStringLiteral("保存异常"), QMessageBox::warning(&window, QStringLiteral("保存异常"),
@ -754,6 +770,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
QStringLiteral("色阶设置开发中。")); 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 配准)── // ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token经同一共享 GeoLocalFrame 配准)──
auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window); auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window);
// 当前底图选择(默认 天地图=Satellite对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。 // 当前底图选择(默认 天地图=Satellite对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。

View File

@ -1,8 +1,13 @@
#include "panels/columns/Column3DAnalysis.hpp" #include "panels/columns/Column3DAnalysis.hpp"
#include <QComboBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu> #include <QMenu>
#include <QSet> #include <QSet>
#include <QSignalBlocker> #include <QSignalBlocker>
#include <QSplitter>
#include <QTreeWidget> #include <QTreeWidget>
#include <QTreeWidgetItem> #include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator> #include <QTreeWidgetItemIterator>
@ -35,7 +40,78 @@ Column3DAnalysis::Column3DAnalysis(QWidget* parent) : QWidget(parent) {
emit checkedItemsChanged(ids); 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<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) { void Column3DAnalysis::setDatasets(const std::vector<geopro::data::DsRow>& rows) {

View File

@ -3,14 +3,16 @@
#include <QStringList> #include <QStringList>
#include <vector> #include <vector>
#include "repo/RepoTypes.hpp" #include "repo/RepoTypes.hpp"
#include "model/Anomaly.hpp"
#include "interact/SlicePlaneMath.hpp" // SliceAxis #include "interact/SlicePlaneMath.hpp" // SliceAxis
class QTreeWidget; class QTreeWidget;
class QComboBox;
class QPoint; class QPoint;
namespace geopro::app { namespace geopro::app {
// 三维分析栏:对象→三维体模型→切片 树 + 三维体/切片两套右键菜单 // 三维分析栏:对象→三维体模型→切片 树 + 三维体/切片两套右键菜单 + 3D 异常控制(列表/过滤/显隐)
class Column3DAnalysis : public QWidget { class Column3DAnalysis : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
@ -19,6 +21,10 @@ public:
void setDatasets(const std::vector<geopro::data::DsRow>& rows); // Analysis 维度(三维体/切片) void setDatasets(const std::vector<geopro::data::DsRow>& rows); // Analysis 维度(三维体/切片)
// 程序化勾选某 dsId 的行(保存切片后自动勾选新行)+ 展开其父节点使可见。 // 程序化勾选某 dsId 的行(保存切片后自动勾选新行)+ 展开其父节点使可见。
void setItemChecked(const QString& dsId, bool checked); void setItemChecked(const QString& dsId, bool checked);
// 3D 异常列表(#4c每条带显隐勾选选中联动 VTK。anoms 为当前应展示的异常集合。
void setAnomalies(const std::vector<geopro::core::Anomaly>& anoms);
// 当前显示过滤档位0全部显示/1随GS/2随数据集/3全部隐藏
int anomalyFilterMode() const;
signals: signals:
void sliceRequested(geopro::render::interact::SliceAxis axis); // 三维体右键 切片▸(上下/前后/左右/任意) void sliceRequested(geopro::render::interact::SliceAxis axis); // 三维体右键 切片▸(上下/前后/左右/任意)
@ -30,10 +36,18 @@ signals:
void sliceExportDatRequested(const QString& dsId); // 导出▸dat void sliceExportDatRequested(const QString& dsId); // 导出▸dat
void sliceDeleteRequested(const QString& dsId); void sliceDeleteRequested(const QString& dsId);
void checkedItemsChanged(const QStringList& dsIds); 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: private:
void onContextMenu(const QPoint& pos); void onContextMenu(const QPoint& pos);
void onAnomalyContextMenu(const QPoint& pos);
QTreeWidget* tree_ = nullptr; QTreeWidget* tree_ = nullptr;
QTreeWidget* anomalyTree_ = nullptr;
QComboBox* anomalyFilter_ = nullptr;
}; };
} // namespace geopro::app } // namespace geopro::app