diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 05716a8..d5bb518 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -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; diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 0ac46c4..b828e24 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -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; diff --git a/src/app/main.cpp b/src/app/main.cpp index c30e561..16bb2ae 100644 --- a/src/app/main.cpp +++ b/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>(); + 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 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 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("色阶"), diff --git a/src/app/panels/columns/Column3DAnalysis.cpp b/src/app/panels/columns/Column3DAnalysis.cpp index dd9ce84..a99f834 100644 --- a/src/app/panels/columns/Column3DAnalysis.cpp +++ b/src/app/panels/columns/Column3DAnalysis.cpp @@ -1,6 +1,7 @@ #include "panels/columns/Column3DAnalysis.hpp" #include +#include #include #include #include @@ -38,20 +39,45 @@ Column3DAnalysis::Column3DAnalysis(QWidget* parent) : QWidget(parent) { } void Column3DAnalysis::setDatasets(const std::vector& rows) { + // 按 dsId 保留刷新前的勾选态:列表重建(保存切片/生成体追加一行也会整树重建)不应丢已勾选项 + // 的渲染态——否则保存切片会连带取消三维体勾选、把它从场景移除(实测 bug)。 + // 切换测线(新数据)时旧 id 不匹配 → 自然全空,行为与原先一致。 + QSet wasChecked; + for (QTreeWidgetItemIterator it(tree_); *it; ++it) + if ((*it)->checkState(0) == Qt::Checked) + wasChecked.insert((*it)->data(0, kDsIdRole).toString()); + { QSignalBlocker blocker(tree_); populateDatasetList(tree_, rows, /*append=*/false); for (QTreeWidgetItemIterator it(tree_); *it; ++it) { (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable); - (*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 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); }); } diff --git a/src/app/panels/columns/Column3DAnalysis.hpp b/src/app/panels/columns/Column3DAnalysis.hpp index c6dd4b4..d6a88db 100644 --- a/src/app/panels/columns/Column3DAnalysis.hpp +++ b/src/app/panels/columns/Column3DAnalysis.hpp @@ -17,15 +17,17 @@ public: explicit Column3DAnalysis(QWidget* parent = nullptr); // 本期:按 ds parentId 建树(切片挂源数据下);完整 对象→三维体→切片 三级树待后端数据(P4)。 void setDatasets(const std::vector& 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); diff --git a/src/data/api/Api3dRepository.cpp b/src/data/api/Api3dRepository.cpp index 876af17..a46a519 100644 --- a/src/data/api/Api3dRepository.cpp +++ b/src/data/api/Api3dRepository.cpp @@ -221,6 +221,17 @@ std::vector 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 onOk, OnError /*onErr*/) { const std::string id = "slice-" + std::to_string(++sliceCounter_); diff --git a/src/data/api/Api3dRepository.hpp b/src/data/api/Api3dRepository.hpp index 4c84e00..58ba50c 100644 --- a/src/data/api/Api3dRepository.hpp +++ b/src/data/api/Api3dRepository.hpp @@ -42,6 +42,10 @@ public: std::vector volumeRows() const; // 已保存切片的列表行(ddCode="dd_slice",parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。 std::vector 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 onOk, diff --git a/src/data/repo/I3dSceneRepository.hpp b/src/data/repo/I3dSceneRepository.hpp index 1e5563b..feba269 100644 --- a/src/data/repo/I3dSceneRepository.hpp +++ b/src/data/repo/I3dSceneRepository.hpp @@ -70,11 +70,15 @@ public: virtual void loadTerrainPaths(std::function 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 origin{{0, 0, 0}}; // 切面上一点(世界米) - std::array normal{{0, 0, 1}}; // 切面法向(单位向量) + int axis = 3; // 轴向(锁旋转用) + std::array origin{{0, 0, 0}}; // 平面 Origin + std::array point1{{0, 0, 0}}; // 平面 Point1 + std::array point2{{0, 0, 0}}; // 平面 Point2 std::string colorScaleId; }; // 切片数据集(持久化态):dsId/名字 + 位姿 + 采样网格。 diff --git a/src/render/interact/InteractionManager.cpp b/src/render/interact/InteractionManager.cpp index 0fdfe13..a8d909e 100644 --- a/src/render/interact/InteractionManager.cpp +++ b/src/render/interact/InteractionManager.cpp @@ -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(axis); + auto tool = std::make_unique(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(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(i)); + selected_ = slices_.empty() ? -1 + : std::min(selected_, static_cast(slices_.size()) - 1); + updateSelectionVisual(); + safeRender(); + return; + } +} + +std::vector InteractionManager::shownSavedSliceIds() const { + std::vector 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(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(slices_.size())) return; + const std::string closedDsId = slices_[static_cast(selected_)]->dsId(); slices_[static_cast(selected_)]->close(); slices_.erase(slices_.begin() + selected_); // 选中停在原位就近(删后该位变成下一张;删的是末张则退一张),不跳回 0(评审 M2)。 @@ -150,6 +199,8 @@ void InteractionManager::closeSelected() { : std::min(selected_, static_cast(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(slices_.size())) return false; const auto& s = slices_[static_cast(selected_)]; - center = s->center(); - normal = s->normal(); + axis = static_cast(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(slices_.size())) return {}; + return slices_[static_cast(selected_)]->dsId(); +} + +void InteractionManager::tagSelectedSlice(const std::string& dsId) { + if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return; + slices_[static_cast(selected_)]->setDsId(dsId); +} + vtkImageData* InteractionManager::selectedSliceImage() const { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; return slices_[static_cast(selected_)]->reslicedOutput(); diff --git a/src/render/interact/InteractionManager.hpp b/src/render/interact/InteractionManager.hpp index fed33a1..74fd471 100644 --- a/src/render/interact/InteractionManager.hpp +++ b/src/render/interact/InteractionManager.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include @@ -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 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 onSliceContextMenuRequested; + // 通过「关闭」显式关掉一张已保存切片时回调其 dsId → 上层据此取消列表勾选(场景↔列表同步)。 + // 仅 closeSelected(用户主动关闭) 触发;closeAll(体变更/清场) 不触发(切片应随体回来再现)。 + std::function onSliceClosed; + // 安装/卸载自定义交互样式(构造时安装;析构卸载恢复原样式)。 void installStyle(); void uninstallStyle(); diff --git a/src/render/interact/SliceTool.cpp b/src/render/interact/SliceTool.cpp index 2c6b182..fe282c5 100644 --- a/src/render/interact/SliceTool.cpp +++ b/src/render/interact/SliceTool.cpp @@ -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::New()) { +void SliceTool::initWidget(const geopro::core::ColorScale& cs, double vmin, double vmax) { // 经 trivial producer 把已存在的 vtkImageData 接入 widget(widget 只暴露 SetInputConnection)。 // producer_ 为成员,随 SliceTool 保活(局部变量会构造后即析构→管线断裂,评审 H1)。 producer_ = vtkSmartPointer::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::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::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& origin, const std::array& point1, + const std::array& point2) + : axis_(axis), image_(image), widget_(vtkSmartPointer::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 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; } diff --git a/src/render/interact/SliceTool.hpp b/src/render/interact/SliceTool.hpp index 6849d29..d5d8189 100644 --- a/src/render/interact/SliceTool.hpp +++ b/src/render/interact/SliceTool.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include @@ -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& origin, const std::array& point1, + const std::array& 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 producer_; vtkSmartPointer widget_; vtkSmartPointer interactObserver_; // 监听 widget StartInteractionEvent → onInteract + void initWidget(const geopro::core::ColorScale& cs, double vmin, double vmax); // 共享 widget 配置 + void applyMarginsAndActivate(); // 按 axis 设旋转锁 + On() + 装交互观察者 std::array imageBounds() const; };