feat(vtk): 切片生命周期重构(3b/3c)-已存切片重渲染+保存链接+场景列表同步
切片"未保存↔已保存"统一状态模型,修复多项交互不一致(用户实测通过): - 保存=链接当前切片到新 dd_slice(不重绘)+三维分析栏自动展开勾选(syncSlices 按 dsId 去重)→ 不再出现"保存后旧渲染还在、再勾选出现两个"的重复切片 - 持久化存精确三点(Origin/Point1/Point2)+axis(SliceSpec),重渲染逐点精确还原 → 尺寸/朝向一致 (修"重渲染切片明显变大") - VTK视图「关闭」已保存切片 → onSliceClosed → 取消列表勾选(场景↔列表双向同步) - VTK视图「保存」按"未保存/已保存"分派:未保存→createSlice+link+autocheck;已保存→saveSlice 覆盖位姿 - 已保存切片在三维分析栏勾选→在当前活动体上按 spec 还原渲染;取消→移除;靠 onVolumeChanged→syncSlices 解决"父体异步到场"排序(SliceTool 还原构造/dsId 标签;InteractionManager showSavedSlice/hideSavedSlice/ selectSavedSlice;Api isSliceDataset/sliceSpec) - 菜单统一/精简:VTK视图与列表导出统一为「导出▸(图片·dat)」;移除列表(三维体/切片)的"显示/隐藏"(勾选即显隐); 列表保存=覆盖位姿、保存为=另存新切片 - 修 Column3DAnalysis::setDatasets:按 dsId 保留勾选态 + 仅勾选集变化才发信号 → 保存切片不再连带取消三维体勾选/重置列表 编译链接绿(build.bat app exit 0);上述场景已用户实测通过。
This commit is contained in:
parent
afdd98f416
commit
d56e35f93d
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
191
src/app/main.cpp
191
src/app/main.cpp
|
|
@ -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("色阶"),
|
||||
|
|
|
|||
|
|
@ -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); });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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_);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -70,11 +70,15 @@ public:
|
|||
virtual void loadTerrainPaths(std::function<void(TerrainPaths)> onOk, OnError onErr) = 0;
|
||||
|
||||
// ── 切片数据集 CRUD(spec §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/名字 + 位姿 + 采样网格。
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 接入 widget(widget 只暴露 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(角度不可调,符合 G22–G24)。
|
||||
// 上下=水平面=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):vtkImagePlaneWidget 用 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(角度不可调,符合 G22–G24)。上下=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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue