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
12 changed files with 397 additions and 107 deletions
Showing only changes of commit d56e35f93d - Show all commits

View File

@ -189,15 +189,6 @@ void VtkSceneView::removeDataset(const std::string& dsId) {
}
}
void VtkSceneView::toggleDatasetVisibility(const std::string& dsId) {
auto it = dsProps_.find(dsId);
if (it == dsProps_.end() || it->second.empty()) return; // 切片(非 dsProps_)等无图元 → 忽略
// 以首个 prop 当前可见性取反,统一作用于该 ds 全部 prop。
const bool show = it->second.front()->GetVisibility() == 0;
for (auto& p : it->second) p->SetVisibility(show ? 1 : 0);
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
int fontSize) {
axesMode_ = mode;

View File

@ -39,8 +39,6 @@ public:
const geopro::core::ColorScale& cs) override;
void addTerrain(const geopro::data::TerrainPaths& paths) override;
void removeDataset(const std::string& dsId) override;
// 切换某数据集图元可见性(三维分析栏「显示/隐藏」;切片等非 dsProps_ 图元忽略)。
void toggleDatasetVisibility(const std::string& dsId);
void setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
int fontSize) override;
void applyCameraView(geopro::controller::ViewDir dir) override;

View File

@ -357,14 +357,39 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
centerLayout->addWidget(viewHeader);
centerLayout->addLayout(viewRow, 1);
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager切片基底
sceneView->onVolumeChanged = [interactionMgr, sceneView]() {
// 3b三维分析栏勾选的已保存切片(dd_slice) id 集合 + 调和函数。
// syncSlices按"当前活动体 dsId"调和 InteractionManager 上显示的已保存切片——
// 勾选且父体=当前体 → 显示(按 spec 还原);否则移除。须在 onVolumeChanged(体到场/移除)末尾
// 及分析栏勾选变化时调用。注setVolumeImage 会 closeAll故体变更后由本函数重建。
auto checkedSliceIds = std::make_shared<std::set<std::string>>();
auto syncSlices = [interactionMgr, sceneView, scene3dRepo, checkedSliceIds]() {
const std::string curVol = sceneView->currentVolumeDsId();
// 移除:已显示但不再需要(未勾选 / 父体非当前体 / 无活动体)。
for (const std::string& shownId : interactionMgr->shownSavedSliceIds()) {
geopro::data::I3dSceneRepository::SliceSpec sp;
const bool wanted = !curVol.empty() && checkedSliceIds->count(shownId) > 0 &&
scene3dRepo->sliceSpec(shownId, sp) && sp.volumeDsId == curVol;
if (!wanted) interactionMgr->hideSavedSlice(shownId);
}
// 添加:勾选 + 父体=当前体 + 未显示showSavedSlice 内部去重)。按精确三点几何还原。
if (!curVol.empty()) {
for (const std::string& id : *checkedSliceIds) {
geopro::data::I3dSceneRepository::SliceSpec sp;
if (scene3dRepo->sliceSpec(id, sp) && sp.volumeDsId == curVol)
interactionMgr->showSavedSlice(id, sp.axis, sp.origin, sp.point1, sp.point2);
}
}
};
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager切片基底并调和已保存切片。
sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices]() {
if (sceneView->hasVolume())
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
sceneView->currentColorScale(), sceneView->currentVmin(),
sceneView->currentVmax());
else
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
syncSlices(); // 体到场/移除后(setVolumeImage 已 closeAll)重建当前体下已勾选的切片
};
// ── 三栏抽屉信号 → 控制器/交互Task 7 接线)──────────────────────────────
@ -393,15 +418,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── VTK 视图切片右键菜单(设计 §2.3)──────────────────────────────────────
// 右键命中切片 → InteractionManager 选中并回调本 lambda → 弹菜单QCursor 处定位)。
// 正视/翻转/关闭=接现有交互;保存=持久化为 dd_slice 进三维分析栏;导出图片/dat=客户端
// 创建异常=占位(#4 接真实后端)
// 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿)导出统一为「导出▸图片·dat」
// 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)
interactionMgr->onSliceContextMenuRequested =
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis]() {
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, drawer]() {
QMenu menu(&window);
QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常"));
QAction* aSave = menu.addAction(QStringLiteral("保存"));
QAction* aImg = menu.addAction(QStringLiteral("导出为图片"));
QAction* aDat = menu.addAction(QStringLiteral("导出到 dat"));
QMenu* expMenu = menu.addMenu(QStringLiteral("导出"));
QAction* aImg = expMenu->addAction(QStringLiteral("图片"));
QAction* aDat = expMenu->addAction(QStringLiteral("dat"));
menu.addSeparator();
QAction* aFace = menu.addAction(QStringLiteral("正视图"));
QAction* aFlip = menu.addAction(QStringLiteral("视图翻转"));
@ -411,17 +437,34 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (chosen == nullptr) return;
if (chosen == aFace) { interactionMgr->faceSelected(); return; }
if (chosen == aFlip) { interactionMgr->flipView(); return; }
if (chosen == aClose) { interactionMgr->closeSelected(); return; }
if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选
if (chosen == aAnomaly) {
QMessageBox::information(&window, QStringLiteral("创建异常"),
QStringLiteral("异常功能开发中(#4切片圈定 + 接真实后端端点)。"));
return;
}
if (chosen == aSave) {
geopro::render::interact::Vec3 c{}, n{};
if (!interactionMgr->selectedSliceSpec(c, n)) return;
const std::string volId = sceneView->currentVolumeDsId();
if (volId.empty()) {
int axis = 3;
geopro::render::interact::Vec3 o{}, p1{}, p2{};
if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return;
geopro::data::I3dSceneRepository::SliceSpec spec;
spec.volumeDsId = sceneView->currentVolumeDsId();
spec.axis = axis;
spec.origin = o;
spec.point1 = p1;
spec.point2 = p2;
const std::string existingId = interactionMgr->selectedSliceDsId();
if (!existingId.empty()) {
// 已保存切片 → 覆盖更新当前位姿(同一「保存」按钮按状态分派)。
scene3dRepo->saveSlice(existingId, spec, []() {},
[&window](const std::string& m) {
QMessageBox::warning(&window, QStringLiteral("保存切片"),
QString::fromStdString(m));
});
return;
}
// 未保存切片 → 新建 dd_slice + 链接当前切片(不重绘) + 列表自动展开勾选(去重不重复)。
if (spec.volumeDsId.empty()) {
QMessageBox::warning(&window, QStringLiteral("保存切片"),
QStringLiteral("当前切片无所属三维体,无法保存。"));
return;
@ -432,13 +475,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
QLineEdit::Normal,
QStringLiteral("切片"), &ok);
if (!ok) return;
geopro::data::I3dSceneRepository::SliceSpec spec;
spec.volumeDsId = volId;
spec.origin = c;
spec.normal = n;
scene3dRepo->createSlice(
spec, name.isEmpty() ? std::string("切片") : name.toStdString(),
[refreshAnalysis](std::string) { refreshAnalysis(); }, // 新切片进三维分析栏
[interactionMgr, refreshAnalysis, drawer](std::string newId) {
interactionMgr->tagSelectedSlice(newId); // 链接当前切片 → 新数据集(不重绘)
refreshAnalysis(); // 新行进列表(勾选集不变→不发多余信号)
drawer->colAnalysis()->setItemChecked(QString::fromStdString(newId),
true); // 自动展开+勾选(syncSlices 去重)
},
[&window](const std::string& m) {
QMessageBox::warning(&window, QStringLiteral("保存切片"),
QString::fromStdString(m));
@ -448,7 +492,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (chosen == aImg) {
vtkSmartPointer<vtkImageData> colorImg = interactionMgr->selectedSliceColorImage();
if (colorImg == nullptr) {
QMessageBox::warning(&window, QStringLiteral("导出为图片"),
QMessageBox::warning(&window, QStringLiteral("导出"),
QStringLiteral("无选中切片或切片无数据。"));
return;
}
@ -457,8 +501,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
QStringLiteral("PNG 图片 (*.png)"));
if (!path.isEmpty() &&
!geopro::app::exportSliceImagePng(colorImg, path.toStdString()))
QMessageBox::warning(&window, QStringLiteral("导出为图片"),
QStringLiteral("导出失败。"));
QMessageBox::warning(&window, QStringLiteral("导出"), QStringLiteral("导出失败。"));
return;
}
if (chosen == aDat) {
@ -469,12 +512,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
QStringLiteral("数据文件 (*.dat)"));
if (!path.isEmpty() &&
!geopro::app::exportSliceDat(img, path.toStdString()))
QMessageBox::warning(&window, QStringLiteral("导出到 dat"),
QStringLiteral("导出失败。"));
QMessageBox::warning(&window, QStringLiteral("导出"), QStringLiteral("导出失败。"));
return;
}
};
// 关闭已保存切片(VTK 视图「关闭」) → 取消三维分析栏对应勾选(场景↔列表双向同步)。
interactionMgr->onSliceClosed = [drawer](const std::string& dsId) {
drawer->colAnalysis()->setItemChecked(QString::fromStdString(dsId), false);
};
QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl,
&geopro::controller::VtkSceneController::setAxesMode);
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
@ -516,11 +563,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
});
auto* ca = drawer->colAnalysis();
// 三维分析栏勾选(三维体/切片)→ 并入渲染勾选集(体走体素路径,由 isVolumeDataset 分流)。
// 三维分析栏勾选(三维体/切片):体走控制器体素路径;切片(dd_slice)不进控制器(否则 loadSection
// 会对 slice id 失败),单独经 syncSlices 在父体上还原渲染。
QObject::connect(ca, &geopro::app::Column3DAnalysis::checkedItemsChanged, sceneCtrl,
[checkedAnalysis, pushChecked](const QStringList& ids) {
*checkedAnalysis = ids;
pushChecked();
[checkedAnalysis, pushChecked, checkedSliceIds, syncSlices,
scene3dRepo](const QStringList& ids) {
QStringList nonSlice;
checkedSliceIds->clear();
for (const QString& id : ids) {
const std::string s = id.toStdString();
if (scene3dRepo->isSliceDataset(s))
checkedSliceIds->insert(s);
else
nonSlice << id;
}
*checkedAnalysis = nonSlice;
pushChecked(); // 体/其它 → 控制器(增删图元,可能触发 onVolumeChanged→syncSlices
syncSlices(); // 切片勾选变化即时调和(父体已在场时立即显隐)
});
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceRequested, vtkWidget,
[interactionMgr](geopro::render::interact::SliceAxis axis) {
@ -530,19 +589,85 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
[&detailCtrl](const QString& dsId, const QString& ddCode, const QString& name) {
detailCtrl.openDataset(dsId, ddCode, name);
});
// 三维分析栏「显示/隐藏」(三维体/帘面图元;切片等非 dsProps_ 图元忽略)。
QObject::connect(ca, &geopro::app::Column3DAnalysis::visibilityToggled, vtkWidget,
[sceneView](const QString& dsId) {
sceneView->toggleDatasetVisibility(dsId.toStdString());
});
// 三维分析栏切片右键「删除」→ 删除 mock 切片 + 刷新列表。
// 三维分析栏切片右键「删除」→ 删除 mock 切片 + 刷新列表(若在渲染,删后行消失→取消勾选→自动移除图元)。
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceDeleteRequested, &window,
[scene3dRepo, refreshAnalysis](const QString& dsId) {
scene3dRepo->deleteSlice(
dsId.toStdString(), [refreshAnalysis]() { refreshAnalysis(); },
[](const std::string&) {});
});
// 色阶 / 切片保存·另存·导出(分析栏入口):本期占位(色阶编辑、已存切片重渲染见 3b/后续)。
// 列表切片「保存」=把当前(可能被拖动过的)位姿覆盖更新到该 dd_slice须该切片正在渲染才有位姿可取。
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceSaveRequested, &window,
[&window, interactionMgr, scene3dRepo, sceneView](const QString& dsId) {
if (!interactionMgr->selectSavedSlice(dsId.toStdString())) {
QMessageBox::information(&window, QStringLiteral("保存"),
QStringLiteral("请先勾选该切片渲染后再保存其位姿。"));
return;
}
int axis = 3;
geopro::render::interact::Vec3 o{}, p1{}, p2{};
interactionMgr->selectedSlicePlane(axis, o, p1, p2);
geopro::data::I3dSceneRepository::SliceSpec spec;
spec.volumeDsId = sceneView->currentVolumeDsId();
spec.axis = axis;
spec.origin = o;
spec.point1 = p1;
spec.point2 = p2;
scene3dRepo->saveSlice(dsId.toStdString(), spec, []() {},
[](const std::string&) {});
});
// 列表切片「保存为」=以该切片当前(存储)位姿另存为新 dd_slice不依赖渲染
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceSaveAsRequested, &window,
[&window, scene3dRepo, refreshAnalysis](const QString& dsId) {
geopro::data::I3dSceneRepository::SliceSpec spec;
if (!scene3dRepo->sliceSpec(dsId.toStdString(), spec)) return;
bool ok = false;
const QString name = QInputDialog::getText(
&window, QStringLiteral("保存为"), QStringLiteral("新切片名称"),
QLineEdit::Normal, QStringLiteral("切片副本"), &ok);
if (!ok) return;
scene3dRepo->createSlice(
spec, name.isEmpty() ? std::string("切片副本") : name.toStdString(),
[refreshAnalysis](std::string) { refreshAnalysis(); },
[](const std::string&) {});
});
// 列表切片「导出▸图片」:定位到渲染中的该切片 → 导出其上色 2D 图。
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceExportImageRequested, &window,
[&window, interactionMgr](const QString& dsId) {
if (!interactionMgr->selectSavedSlice(dsId.toStdString())) {
QMessageBox::information(&window, QStringLiteral("导出"),
QStringLiteral("请先勾选该切片渲染后再导出。"));
return;
}
vtkSmartPointer<vtkImageData> img = interactionMgr->selectedSliceColorImage();
if (img == nullptr) return;
const QString path = QFileDialog::getSaveFileName(
&window, QStringLiteral("导出为图片"), QStringLiteral("slice.png"),
QStringLiteral("PNG 图片 (*.png)"));
if (!path.isEmpty() &&
!geopro::app::exportSliceImagePng(img, path.toStdString()))
QMessageBox::warning(&window, QStringLiteral("导出"),
QStringLiteral("导出失败。"));
});
// 列表切片「导出▸dat」定位到渲染中的该切片 → 导出其重采样标量网格。
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceExportDatRequested, &window,
[&window, interactionMgr](const QString& dsId) {
if (!interactionMgr->selectSavedSlice(dsId.toStdString())) {
QMessageBox::information(&window, QStringLiteral("导出"),
QStringLiteral("请先勾选该切片渲染后再导出。"));
return;
}
vtkImageData* img = interactionMgr->selectedSliceImage();
if (img == nullptr) return;
const QString path = QFileDialog::getSaveFileName(
&window, QStringLiteral("导出到 dat"), QStringLiteral("slice.dat"),
QStringLiteral("数据文件 (*.dat)"));
if (!path.isEmpty() &&
!geopro::app::exportSliceDat(img, path.toStdString()))
QMessageBox::warning(&window, QStringLiteral("导出"),
QStringLiteral("导出失败。"));
});
// 色阶(三维体/切片):本期占位。
QObject::connect(ca, &geopro::app::Column3DAnalysis::colorScaleRequested, &window,
[&window](const QString&) {
QMessageBox::information(&window, QStringLiteral("色阶"),

View File

@ -1,6 +1,7 @@
#include "panels/columns/Column3DAnalysis.hpp"
#include <QMenu>
#include <QSet>
#include <QSignalBlocker>
#include <QTreeWidget>
#include <QTreeWidgetItem>
@ -38,20 +39,45 @@ Column3DAnalysis::Column3DAnalysis(QWidget* parent) : QWidget(parent) {
}
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);
(*it)->setCheckState(0, Qt::Unchecked);
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)
ids << (*it)->data(0, kDsIdRole).toString();
emit checkedItemsChanged(ids);
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) {
@ -65,7 +91,8 @@ void Column3DAnalysis::onContextMenu(const QPoint& pos) {
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); });
@ -73,17 +100,18 @@ void Column3DAnalysis::onContextMenu(const QPoint& pos) {
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]{ emit visibilityToggled(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); });
menu.addAction(QStringLiteral("导出"), this, [this, dsId]{ emit sliceExportRequested(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]{ emit visibilityToggled(dsId); });
menu.addAction(QStringLiteral("数据详情"), this, [this, dsId, ddCode, name]{ emit detailRequested(dsId, ddCode, name); });
}

View File

@ -17,15 +17,17 @@ public:
explicit Column3DAnalysis(QWidget* parent = nullptr);
// 本期:按 ds parentId 建树(切片挂源数据下);完整 对象→三维体→切片 三级树待后端数据(P4)。
void setDatasets(const std::vector<geopro::data::DsRow>& rows); // Analysis 维度(三维体/切片)
// 程序化勾选某 dsId 的行(保存切片后自动勾选新行)+ 展开其父节点使可见。
void setItemChecked(const QString& dsId, bool checked);
signals:
void sliceRequested(geopro::render::interact::SliceAxis axis); // 三维体右键 切片▸(上下/前后/左右/任意)
void colorScaleRequested(const QString& dsId);
void visibilityToggled(const QString& dsId);
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name);
void sliceSaveRequested(const QString& dsId);
void sliceSaveAsRequested(const QString& dsId);
void sliceExportRequested(const QString& dsId);
void sliceExportImageRequested(const QString& dsId); // 导出▸图片
void sliceExportDatRequested(const QString& dsId); // 导出▸dat
void sliceDeleteRequested(const QString& dsId);
void checkedItemsChanged(const QStringList& dsIds);

View File

@ -221,6 +221,17 @@ std::vector<DsRow> Api3dRepository::sliceRows() const {
return rows;
}
bool Api3dRepository::isSliceDataset(const std::string& dsId) const {
return slices_.find(dsId) != slices_.end();
}
bool Api3dRepository::sliceSpec(const std::string& dsId, SliceSpec& out) const {
auto it = slices_.find(dsId);
if (it == slices_.end()) return false;
out = it->second.spec;
return true;
}
void Api3dRepository::createSlice(const SliceSpec& spec, const std::string& name,
std::function<void(std::string)> onOk, OnError /*onErr*/) {
const std::string id = "slice-" + std::to_string(++sliceCounter_);

View File

@ -42,6 +42,10 @@ public:
std::vector<DsRow> volumeRows() const;
// 已保存切片的列表行ddCode="dd_slice"parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。
std::vector<DsRow> sliceRows() const;
// 该 dsId 是否为已保存切片3b分析栏勾选 dd_slice 走切片重渲染路径,不进控制器帘面/体素路径)。
bool isSliceDataset(const std::string& dsId) const;
// 取回已保存切片位姿(还原渲染用);不存在返回 false。
bool sliceSpec(const std::string& dsId, SliceSpec& out) const;
void loadVolume(const std::string& dsId,
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,

View File

@ -70,11 +70,15 @@ public:
virtual void loadTerrainPaths(std::function<void(TerrainPaths)> onOk, OnError onErr) = 0;
// ── 切片数据集 CRUDspec §6.3)──────────────────────────────────────────
// 切面位姿(原点 + 法向,用 std::array 去裸 double[])。
// 切面精确几何vtkImagePlaneWidget 的三点(Origin/Point1/Point2) + 轴向 →
// 重渲染逐点精确还原(尺寸/朝向/位置一致);法向 = normalize((p1-o)×(p2-o)),可派生。
// axis: 0 上下 / 1 前后 / 2 左右 / 3 任意(=render::interact::SliceAxis 顺序);决定还原时是否锁旋转。
struct SliceSpec {
std::string volumeDsId; // 所属三维体 dsId
std::array<double, 3> origin{{0, 0, 0}}; // 切面上一点(世界米)
std::array<double, 3> normal{{0, 0, 1}}; // 切面法向(单位向量)
int axis = 3; // 轴向(锁旋转用)
std::array<double, 3> origin{{0, 0, 0}}; // 平面 Origin
std::array<double, 3> point1{{0, 0, 0}}; // 平面 Point1
std::array<double, 3> point2{{0, 0, 0}}; // 平面 Point2
std::string colorScaleId;
};
// 切片数据集持久化态dsId/名字 + 位姿 + 采样网格。

View File

@ -117,6 +117,54 @@ void InteractionManager::addSlice(SliceAxis axis) {
safeRender();
}
void InteractionManager::showSavedSlice(const std::string& dsId, int axis, const Vec3& origin,
const Vec3& point1, const Vec3& point2) {
if (!image_ || !interactor_ || dsId.empty()) return;
for (const auto& s : slices_)
if (s->dsId() == dsId) return; // 已显示 → 去重跳过
const SliceAxis ax = static_cast<SliceAxis>(axis);
auto tool = std::make_unique<SliceTool>(image_, interactor_, ax, colorScale_, vmin_, vmax_,
origin, point1, point2); // 三点精确还原
tool->setDsId(dsId);
SliceTool* tp = tool.get();
tool->onInteract = [this, tp]() { selectByTool(tp); };
slices_.push_back(std::move(tool));
selected_ = static_cast<int>(slices_.size()) - 1;
updateSelectionVisual();
safeRender();
}
void InteractionManager::hideSavedSlice(const std::string& dsId) {
for (std::size_t i = 0; i < slices_.size(); ++i) {
if (slices_[i]->dsId() != dsId) continue;
slices_[i]->close();
slices_.erase(slices_.begin() + static_cast<long>(i));
selected_ = slices_.empty() ? -1
: std::min(selected_, static_cast<int>(slices_.size()) - 1);
updateSelectionVisual();
safeRender();
return;
}
}
std::vector<std::string> InteractionManager::shownSavedSliceIds() const {
std::vector<std::string> out;
for (const auto& s : slices_)
if (!s->dsId().empty()) out.push_back(s->dsId());
return out;
}
bool InteractionManager::selectSavedSlice(const std::string& dsId) {
for (std::size_t i = 0; i < slices_.size(); ++i) {
if (slices_[i]->dsId() != dsId) continue;
selected_ = static_cast<int>(i);
updateSelectionVisual();
safeRender();
return true;
}
return false;
}
void InteractionManager::selectByTool(const SliceTool* tool) {
int idx = -1;
for (std::size_t i = 0; i < slices_.size(); ++i)
@ -143,6 +191,7 @@ void InteractionManager::selectByTool(const SliceTool* tool) {
void InteractionManager::closeSelected() {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
const std::string closedDsId = slices_[static_cast<std::size_t>(selected_)]->dsId();
slices_[static_cast<std::size_t>(selected_)]->close();
slices_.erase(slices_.begin() + selected_);
// 选中停在原位就近(删后该位变成下一张;删的是末张则退一张),不跳回 0评审 M2
@ -150,6 +199,8 @@ void InteractionManager::closeSelected() {
: std::min(selected_, static_cast<int>(slices_.size()) - 1);
updateSelectionVisual();
safeRender();
// 已保存切片被主动关闭 → 通知上层取消列表勾选(场景↔列表同步)。
if (!closedDsId.empty() && onSliceClosed) onSliceClosed(closedDsId);
}
void InteractionManager::closeAll() {
@ -170,14 +221,29 @@ void InteractionManager::flipView() {
void InteractionManager::faceSelected() { faceSlice(selected_); }
bool InteractionManager::selectedSliceSpec(Vec3& center, Vec3& normal) const {
bool InteractionManager::selectedSlicePlane(int& axis, Vec3& origin, Vec3& point1,
Vec3& point2) const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
const auto& s = slices_[static_cast<std::size_t>(selected_)];
center = s->center();
normal = s->normal();
axis = static_cast<int>(s->axis());
double o[3], p1[3], p2[3];
s->planePoints(o, p1, p2);
origin = {{o[0], o[1], o[2]}};
point1 = {{p1[0], p1[1], p1[2]}};
point2 = {{p2[0], p2[1], p2[2]}};
return true;
}
std::string InteractionManager::selectedSliceDsId() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return {};
return slices_[static_cast<std::size_t>(selected_)]->dsId();
}
void InteractionManager::tagSelectedSlice(const std::string& dsId) {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
slices_[static_cast<std::size_t>(selected_)]->setDsId(dsId);
}
vtkImageData* InteractionManager::selectedSliceImage() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
return slices_[static_cast<std::size_t>(selected_)]->reslicedOutput();

View File

@ -1,6 +1,7 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include <vtkSmartPointer.h>
@ -45,6 +46,16 @@ public:
// 创建一张切片(轴向/任意)。无体素 image 则忽略。新切片自动设为选中。
void addSlice(SliceAxis axis);
// ── 已保存切片(dd_slice)按 dsId 显隐3b三维分析栏勾选/取消已保存切片)──────────
// showSavedSlice在当前体上按精确三点几何还原一张带 dsId 标签的切片;已显示则跳过(去重)。
// 须在父体 image 已 setVolumeImage 后调用(无 image 则忽略。axis 仅决定是否锁旋转。
void showSavedSlice(const std::string& dsId, int axis, const Vec3& origin, const Vec3& point1,
const Vec3& point2);
void hideSavedSlice(const std::string& dsId);
std::vector<std::string> shownSavedSliceIds() const; // 当前已显示的已保存切片 dsId 列表
// 选中已显示的某 dsId 切片(列表操作定位到对应渲染切片);找到返回 true。
bool selectSavedSlice(const std::string& dsId);
// 关闭选中切片E56。无选中则忽略。
void closeSelected();
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
@ -59,8 +70,12 @@ public:
// 正视当前选中切片(菜单「正视图」入口;无选中则忽略)。
void faceSelected();
// 选中切片位姿(保存切片用):有选中→填 center/normal(世界系) 返回 true否则 false。
bool selectedSliceSpec(Vec3& center, Vec3& normal) const;
// 选中切片精确平面(保存用):有选中→填 axis + 三点 返回 true否则 false。
bool selectedSlicePlane(int& axis, Vec3& origin, Vec3& point1, Vec3& point2) const;
// 选中切片的归属 dsId已保存切片非空未保存为空。无选中返回空字符串。
std::string selectedSliceDsId() const;
// 给当前选中(未保存)切片打 dsId 标签:保存=把当前切片链接到新数据集(不重绘、不重复)。
void tagSelectedSlice(const std::string& dsId);
// 选中切片的重采样 2D 标量影像(导出 dat 用);无选中返回 nullptr。
vtkImageData* selectedSliceImage() const;
// 选中切片"上色后"的 2D 影像(导出图片用):重采样标量经切片色阶 LUT → RGB 图;无选中返回 nullptr。
@ -69,6 +84,10 @@ public:
// 右键命中切片时回调manager 已选中所在切片)→ 上层据此弹切片右键菜单(用 QCursor::pos 定位)。
std::function<void()> onSliceContextMenuRequested;
// 通过「关闭」显式关掉一张已保存切片时回调其 dsId → 上层据此取消列表勾选(场景↔列表同步)。
// 仅 closeSelected(用户主动关闭) 触发closeAll(体变更/清场) 不触发(切片应随体回来再现)。
std::function<void(const std::string& dsId)> onSliceClosed;
// 安装/卸载自定义交互样式(构造时安装;析构卸载恢复原样式)。
void installStyle();
void uninstallStyle();

View File

@ -21,16 +21,13 @@ namespace {
constexpr double kSqrt2Inv = 0.70710678118654752440;
} // namespace
SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
const geopro::core::ColorScale& cs, double vmin, double vmax)
: axis_(axis), image_(image), widget_(vtkSmartPointer<vtkImagePlaneWidget>::New()) {
void SliceTool::initWidget(const geopro::core::ColorScale& cs, double vmin, double vmax) {
// 经 trivial producer 把已存在的 vtkImageData 接入 widgetwidget 只暴露 SetInputConnection
// producer_ 为成员,随 SliceTool 保活(局部变量会构造后即析构→管线断裂,评审 H1
producer_ = vtkSmartPointer<vtkTrivialProducer>::New();
producer_->SetOutput(image_);
widget_->SetInputConnection(producer_->GetOutputPort());
widget_->SetInteractor(interactor);
widget_->RestrictPlaneToVolumeOn(); // 切面限制在体内,滚轮推进不跑飞
widget_->SetResliceInterpolateToLinear(); // reslice 线性插值出连续剖面(非 cutter 交线)
widget_->TextureInterpolateOn();
@ -39,45 +36,9 @@ SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor,
// 色阶 LUT 套用:用户自管 LUT不让 widget 用默认灰度窗位)。
auto lut = buildLut(cs, vmin, vmax);
widget_->SetLookupTable(lut);
}
// 轴向:固定到 X/Y/Z角度不可调符合 G22G24
// 上下=水平面=Z 法向;前后=Y 法向;左右=X 法向。
switch (axis_) {
case SliceAxis::UpDown:
widget_->SetPlaneOrientationToZAxes();
break;
case SliceAxis::FrontBack:
widget_->SetPlaneOrientationToYAxes();
break;
case SliceAxis::LeftRight:
widget_->SetPlaneOrientationToXAxes();
break;
case SliceAxis::Oblique: {
// 任意 45°F25vtkImagePlaneWidget 用 Origin/Point1/Point2 三角点定义平面
// (无 SetNormal。法向 = (Point1-Origin)×(Point2-Origin)。
// 取法向 (sin45,0,cos45)in-plane 轴1 = Y(0,1,0)轴2 = XZ 内与法向正交方向 (cos45,0,-sin45)。
// 以体中心为面心,沿两轴各展半个体范围,得一张斜插体的对角面(可继续交互旋转)。
const auto b = imageBounds();
const double cx = 0.5 * (b[0] + b[1]);
const double cy = 0.5 * (b[2] + b[3]);
const double cz = 0.5 * (b[4] + b[5]);
const double hy = 0.5 * (b[3] - b[2]);
// 轴2 半长取 X/Z 范围的较大者,保证面铺满体对角。
const double hxz = 0.5 * std::max(b[1] - b[0], b[5] - b[4]);
// 轴1 = +Y轴2 = (cos45,0,-sin45)。
const double a2x = kSqrt2Inv, a2z = -kSqrt2Inv;
// Origin = center - 0.5*axis1 - 0.5*axis2使 center 为面心)。
const double ox = cx - 0.0 - a2x * hxz;
const double oy = cy - hy - 0.0;
const double oz = cz - 0.0 - a2z * hxz;
widget_->SetOrigin(ox, oy, oz);
widget_->SetPoint1(ox + 0.0, oy + 2.0 * hy, oz + 0.0); // 沿 +Y
widget_->SetPoint2(ox + a2x * 2.0 * hxz, oy, oz + a2z * 2.0 * hxz); // 沿 (cos45,0,-sin45)
widget_->UpdatePlacement();
break;
}
}
void SliceTool::applyMarginsAndActivate() {
// 左键拖动=移动切面(默认左键是窗位调整,无用);中键=取值光标。
widget_->SetLeftButtonAction(vtkImagePlaneWidget::VTK_SLICE_MOTION_ACTION);
widget_->SetMiddleButtonAction(vtkImagePlaneWidget::VTK_CURSOR_ACTION);
@ -89,7 +50,6 @@ SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor,
}
widget_->On();
// 保持 widget 交互开启:任意切片可拖动调整角度/位置(F25 '可任意调整')。
// 监听其交互开始事件 → 触碰本切片即回调 onInteract(上层据此设为选中)。
interactObserver_ = vtkSmartPointer<vtkCallbackCommand>::New();
interactObserver_->SetClientData(this);
@ -100,6 +60,61 @@ SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor,
widget_->AddObserver(vtkCommand::StartInteractionEvent, interactObserver_);
}
SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
const geopro::core::ColorScale& cs, double vmin, double vmax)
: axis_(axis), image_(image), widget_(vtkSmartPointer<vtkImagePlaneWidget>::New()) {
initWidget(cs, vmin, vmax);
widget_->SetInteractor(interactor);
// 轴向:固定到 X/Y/Z角度不可调符合 G22G24。上下=Z 法向;前后=Y 法向;左右=X 法向。
switch (axis_) {
case SliceAxis::UpDown:
widget_->SetPlaneOrientationToZAxes();
break;
case SliceAxis::FrontBack:
widget_->SetPlaneOrientationToYAxes();
break;
case SliceAxis::LeftRight:
widget_->SetPlaneOrientationToXAxes();
break;
case SliceAxis::Oblique: {
// 任意 45°F25用 Origin/Point1/Point2 三点定义平面。法向 (sin45,0,cos45)
// in-plane 轴1=Y(0,1,0)轴2=(cos45,0,-sin45);以体中心为面心、铺满体对角。
const auto b = imageBounds();
const double cx = 0.5 * (b[0] + b[1]);
const double cy = 0.5 * (b[2] + b[3]);
const double cz = 0.5 * (b[4] + b[5]);
const double hy = 0.5 * (b[3] - b[2]);
const double hxz = 0.5 * std::max(b[1] - b[0], b[5] - b[4]);
const double a2x = kSqrt2Inv, a2z = -kSqrt2Inv;
const double ox = cx - a2x * hxz;
const double oy = cy - hy;
const double oz = cz - a2z * hxz;
widget_->SetOrigin(ox, oy, oz);
widget_->SetPoint1(ox, oy + 2.0 * hy, oz); // 沿 +Y
widget_->SetPoint2(ox + a2x * 2.0 * hxz, oy, oz + a2z * 2.0 * hxz); // 沿 (cos45,0,-sin45)
widget_->UpdatePlacement();
break;
}
}
applyMarginsAndActivate();
}
SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
const geopro::core::ColorScale& cs, double vmin, double vmax,
const std::array<double, 3>& origin, const std::array<double, 3>& point1,
const std::array<double, 3>& point2)
: axis_(axis), image_(image), widget_(vtkSmartPointer<vtkImagePlaneWidget>::New()) {
initWidget(cs, vmin, vmax);
widget_->SetInteractor(interactor);
// 还原:直接用保存的精确三点(不做轴向 snap保证尺寸/朝向/位置与保存时一致。
widget_->SetOrigin(origin[0], origin[1], origin[2]);
widget_->SetPoint1(point1[0], point1[1], point1[2]);
widget_->SetPoint2(point2[0], point2[1], point2[2]);
widget_->UpdatePlacement();
applyMarginsAndActivate(); // 按 axis 锁旋转(轴向切片仍不可旋转)
}
SliceTool::~SliceTool() { close(); }
std::array<double, 6> SliceTool::imageBounds() const {
@ -141,6 +156,16 @@ void SliceTool::advance(double step) {
widget_->UpdatePlacement();
}
void SliceTool::planePoints(double origin[3], double point1[3], double point2[3]) const {
if (!widget_) {
for (int i = 0; i < 3; ++i) origin[i] = point1[i] = point2[i] = 0.0;
return;
}
widget_->GetOrigin(origin);
widget_->GetPoint1(point1);
widget_->GetPoint2(point2);
}
vtkImageData* SliceTool::reslicedOutput() const {
return widget_ ? widget_->GetResliceOutput() : nullptr;
}

View File

@ -1,6 +1,7 @@
#pragma once
#include <array>
#include <functional>
#include <string>
#include <vtkSmartPointer.h>
@ -30,6 +31,12 @@ public:
// axis切面方向。vmin/vmax色阶区间。
SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
const geopro::core::ColorScale& cs, double vmin, double vmax);
// 还原构造(已保存切片按 spec 重渲染用精确三点几何axis 仅决定是否锁旋转(不做轴向 snap
// 尺寸/朝向/位置与保存时完全一致。
SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
const geopro::core::ColorScale& cs, double vmin, double vmax,
const std::array<double, 3>& origin, const std::array<double, 3>& point1,
const std::array<double, 3>& point2);
~SliceTool();
SliceTool(const SliceTool&) = delete;
@ -39,6 +46,13 @@ public:
SliceAxis axis() const { return axis_; }
// 已保存切片(dd_slice)还原时打的归属标签;临时(交互新建)切片为空。供按 dsId 显隐/去重。
const std::string& dsId() const { return dsId_; }
void setDsId(std::string id) { dsId_ = std::move(id); }
// 取当前切面精确三点(保存用)。
void planePoints(double origin[3], double point1[3], double point2[3]) const;
// 当前切面法向(世界系单位向量)。
Vec3 normal() const;
// 当前切面中心origin
@ -65,12 +79,15 @@ public:
private:
SliceAxis axis_;
std::string dsId_; // 已保存切片归属标签(空=临时交互切片)
vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证
// 把已存在的 image 接入 widget 的 producer须随 SliceTool 保活(否则构造后析构→管线断裂崩溃,评审 H1
vtkSmartPointer<vtkTrivialProducer> producer_;
vtkSmartPointer<vtkImagePlaneWidget> widget_;
vtkSmartPointer<vtkCallbackCommand> interactObserver_; // 监听 widget StartInteractionEvent → onInteract
void initWidget(const geopro::core::ColorScale& cs, double vmin, double vmax); // 共享 widget 配置
void applyMarginsAndActivate(); // 按 axis 设旋转锁 + On() + 装交互观察者
std::array<double, 6> imageBounds() const;
};