From 69e87908100186f5ae2ba032167cae9b5ecc3c31 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Thu, 25 Jun 2026 20:36:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(vtk):=20=E5=A4=9A=E4=B8=89=E7=BB=B4?= =?UTF-8?q?=E4=BD=93=E5=B9=B6=E5=8F=91=E5=88=87=E7=89=87=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E9=87=8D=E6=9E=84(OPT-002:issue2+=E2=91=A2+=E5=8F=8D=E5=90=91?= =?UTF-8?q?=E2=91=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心:InteractionManager 单 image_ → 按 volumeDsId 的多体 map;各切片附着到各自体的 image。 - issue2 选第二个体→第一个体切片消失:setVolumeImage 不再 closeAll 全部切片;改 upsert 某体(同体重建 才关该体切片)。syncSlices 改为「勾选 + 父体已渲染」即显示(不再限当前体)→ 多体切片并存 - ③ 右键体却建到 current 体:sliceRequested 带目标体 dsId;addSlice(axis,volumeDsId) 用该体 image; 保存切片/创建异常的 volumeDsId 改用 selectedSliceVolumeDsId(选中切片所属体)而非 currentVolume - 反向② VTK→树:InteractionManager.onSliceSelectionChanged(selectByTool/onPicked 触发)→ CategorySection::selectItem 程序化选中(屏蔽信号防环路) - VtkSceneView 按 dsId 存 volumes_(image/cs/vmin/vmax);addVolume 登记/removeDataset 移除并回退 current/clear 清空;volume(dsId) 取指定体→色阶编辑支持非当前体;SliceTool 加 volumeDsId 标签 - nearestSlice 阈值/onWheel 步长/导出上色 改用切片所属体的 bounds/色阶 未解:④ 切片拾取串选(nearestSlice 启发式,VTK9.6 不暴露切片 actor 难精确化;widget 交互选中是精确的) 构建:app 链接通过;434/434 测试通过 --- src/app/VtkSceneView.cpp | 20 +++- src/app/VtkSceneView.hpp | 20 +++- src/app/main.cpp | 67 +++++++------ .../panels/columns/CategoryAnalysisTab.hpp | 2 +- src/app/panels/columns/CategorySection.cpp | 20 +++- src/app/panels/columns/CategorySection.hpp | 3 +- src/render/interact/InteractionManager.cpp | 94 +++++++++++++++---- src/render/interact/InteractionManager.hpp | 44 ++++++--- src/render/interact/SliceTool.hpp | 7 +- 9 files changed, 206 insertions(+), 71 deletions(-) diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 4a277f8..3b2fddf 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -111,6 +111,7 @@ void VtkSceneView::clear() { // 体素 image 失效:置空并通知上层关闭切片(防切片附着到已移除的 image)。 currentVolumeImage_ = nullptr; volumeOwnerDs_.clear(); + volumes_.clear(); // 多体并发:清场移除所有体 image frameAnchoredToData_ = false; // 新一轮选择重新按其首个真实剖面重锚原点 if (onVolumeChanged) onVolumeChanged(); } @@ -170,6 +171,7 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume currentVmin_ = vol.vmin; currentVmax_ = vol.vmax; volumeOwnerDs_ = dsId; + volumes_[dsId] = VolumeRec{image, cs, vol.vmin, vol.vmax}; // 多体并发:登记本体 image if (onVolumeChanged) onVolumeChanged(); } } @@ -201,11 +203,21 @@ void VtkSceneView::removeDataset(const std::string& dsId) { if (it == dsProps_.end()) return; removeProps(it->second); dsProps_.erase(it); - if (volumeOwnerDs_ == dsId) { // 该 ds 的体素被移除 → 切片源失效 - currentVolumeImage_ = nullptr; - volumeOwnerDs_.clear(); - if (onVolumeChanged) onVolumeChanged(); + const bool wasVolume = volumes_.erase(dsId) > 0; + if (volumeOwnerDs_ == dsId) { // 移除的是"当前体" → currentImage 回退到剩余某体,无则置空 + if (!volumes_.empty()) { + const auto& last = *volumes_.rbegin(); + volumeOwnerDs_ = last.first; + currentVolumeImage_ = last.second.image; + currentColorScale_ = last.second.cs; + currentVmin_ = last.second.vmin; + currentVmax_ = last.second.vmax; + } else { + currentVolumeImage_ = nullptr; + volumeOwnerDs_.clear(); + } } + if (wasVolume && onVolumeChanged) onVolumeChanged(); // 任一体移除 → 上层多体同步切片 } void VtkSceneView::addAnomaly(const geopro::core::Anomaly& a) { diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 9f89fa9..bff287d 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -116,11 +116,29 @@ private: // 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。 vtkSmartPointer currentAxes_; - // 当前体素 image + 色阶(P3 切片附着源);无体素时为空。 + // 当前体素 image + 色阶(P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/ + // 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。 vtkSmartPointer currentVolumeImage_; geopro::core::ColorScale currentColorScale_; double currentVmin_ = 0.0; double currentVmax_ = 0.0; + // 多体并发:按 dsId 持各已渲染体的 image + 色阶(供 InteractionManager 让各体切片各用自己的 image)。 + struct VolumeRec { + vtkSmartPointer image; + geopro::core::ColorScale cs; + double vmin = 0.0, vmax = 0.0; + }; + std::map volumes_; + +public: + const std::map& volumes() const { return volumes_; } // 已渲染各体 image/色阶 + bool isVolumeRendered(const std::string& dsId) const { return volumes_.count(dsId) > 0; } + const VolumeRec* volume(const std::string& dsId) const { // 取指定已渲染体的 image/色阶(缺=nullptr) + auto it = volumes_.find(dsId); + return it != volumes_.end() ? &it->second : nullptr; + } + +private: // 增量渲染:按 dsId 跟踪该数据集的 props(帘面/体素),支持单独移除而不全量重建; // miscProps_ 为非数据集 prop(地形/测线),仅随 clear 全量移除。底图由 TileBasemap 自管、不在此。 diff --git a/src/app/main.cpp b/src/app/main.cpp index e7314a5..481de59 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -409,22 +409,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 勾选且父体=当前体 → 显示(按 spec 还原);否则移除。须在 onVolumeChanged(体到场/移除)末尾 // 及分析栏勾选变化时调用。注:setVolumeImage 会 closeAll,故体变更后由本函数重建。 auto checkedSliceIds = std::make_shared>(); - auto syncSlices = [interactionMgr, sceneView, scene3dRepo, checkedSliceIds]() { - const std::string curVol = sceneView->currentVolumeDsId(); - // 移除:已显示但不再需要(未勾选 / 父体非当前体 / 无活动体)。 + auto syncSlices = [interactionMgr, scene3dRepo, checkedSliceIds]() { + // 多体并发:切片只要勾选 + 其父体已渲染就显示(不再限定"当前体")→ 多个体的切片可并存。 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; + const bool wanted = checkedSliceIds->count(shownId) > 0 && + scene3dRepo->sliceSpec(shownId, sp) && + interactionMgr->isVolumeRendered(sp.volumeDsId); 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); - } + for (const std::string& id : *checkedSliceIds) { + geopro::data::I3dSceneRepository::SliceSpec sp; + if (scene3dRepo->sliceSpec(id, sp) && interactionMgr->isVolumeRendered(sp.volumeDsId)) + interactionMgr->showSavedSlice(id, sp.axis, sp.origin, sp.point1, sp.point2, + sp.volumeDsId); } }; @@ -455,13 +453,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 体素变化(重建/清场)后把体素 image 推给 InteractionManager(切片基底),并调和已保存切片 + 异常。 sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, refreshAnomalies]() { - if (sceneView->hasVolume()) - interactionMgr->setVolumeImage(sceneView->currentVolumeImage(), - sceneView->currentColorScale(), sceneView->currentVmin(), - sceneView->currentVmax()); - else - interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0); - syncSlices(); // 体到场/移除后重建当前体下已勾选的切片 + // 多体并发:先移除 interactionMgr 中已不再渲染的体(关其切片),再 upsert 当前所有已渲染体 image。 + for (const std::string& id : interactionMgr->volumeIds()) + if (!sceneView->isVolumeRendered(id)) interactionMgr->removeVolumeImage(id); + for (const auto& kv : sceneView->volumes()) + interactionMgr->setVolumeImage(kv.first, kv.second.image.Get(), kv.second.cs, + kv.second.vmin, kv.second.vmax); + syncSlices(); // 体到场/移除后调和各体下已勾选切片(多体并存) refreshAnomalies(); // 同步重载异常 actor + 刷新异常列表 }; @@ -532,7 +530,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const ri::Vec3 e1{{p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]}}; const ri::Vec3 e2{{p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]}}; const ri::Vec3 normal = ri::normalize(ri::cross(e1, e2)); - const std::string volId = sceneView->currentVolumeDsId(); + // 多体并发:异常挂到"选中切片所属体"(非 currentVolume),无选中切片回退当前体。 + std::string volId = interactionMgr->selectedSliceVolumeDsId(); + if (volId.empty()) volId = sceneView->currentVolumeDsId(); // 异常归属(spec §8):当前选中切片已保存(selectedSliceDsId 非空)→挂该切片;临时切片→挂体。 const std::string savedSliceId = interactionMgr->selectedSliceDsId(); anomalyDrawTool->start( @@ -592,7 +592,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re geopro::render::interact::Vec3 o{}, p1{}, p2{}; if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return; geopro::data::I3dSceneRepository::SliceSpec spec; - spec.volumeDsId = sceneView->currentVolumeDsId(); + // 多体并发:切片归属"选中切片所属体"(非 currentVolume),无则回退当前体。 + spec.volumeDsId = interactionMgr->selectedSliceVolumeDsId(); + if (spec.volumeDsId.empty()) spec.volumeDsId = sceneView->currentVolumeDsId(); spec.axis = axis; spec.origin = o; spec.point1 = p1; @@ -801,8 +803,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // (O点位置/字体、旧栏「生成三维体」「勾选→渲染」接线均已退役——分别由 analysisTab 的 // generateVolumeRequested / checkedDatasetsChanged 接管。) QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::sliceRequested, vtkWidget, - [interactionMgr](geopro::render::interact::SliceAxis axis) { - interactionMgr->addSlice(axis); + [interactionMgr](geopro::render::interact::SliceAxis axis, const QString& volumeDsId) { + // 切片建到被右键的三维体上(③:不再用 currentVolume)。该体须已渲染(有 image)。 + interactionMgr->addSlice(axis, volumeDsId.toStdString()); }); // 列表切片「保存」=把当前(可能被拖动过的)位姿覆盖更新到该 dd_slice;须该切片正在渲染才有位姿可取。 QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::sliceSaveRequested, &window, @@ -816,7 +819,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re geopro::render::interact::Vec3 o{}, p1{}, p2{}; interactionMgr->selectedSlicePlane(axis, o, p1, p2); geopro::data::I3dSceneRepository::SliceSpec spec; - spec.volumeDsId = sceneView->currentVolumeDsId(); + // 多体并发:保位姿归属"该切片所属体"(非 currentVolume)。 + spec.volumeDsId = interactionMgr->selectedSliceVolumeDsId(); + if (spec.volumeDsId.empty()) spec.volumeDsId = sceneView->currentVolumeDsId(); spec.axis = axis; spec.origin = o; spec.point1 = p1; @@ -880,7 +885,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::colorScaleRequested, &window, [&window, &colorTplRepo, &nav, sceneCtrl, sceneView](const QString& qid) { const std::string dsId = qid.toStdString(); - if (sceneView->currentVolumeDsId() != dsId || !sceneView->hasVolume()) { + // 多体并发:编辑"该体"(任一已渲染体,不限当前体)的色阶。 + const auto* vol = sceneView->volume(dsId); + if (!vol) { QMessageBox::information( &window, QStringLiteral("色阶"), QStringLiteral("请先勾选该三维体使其渲染后再编辑色阶。")); @@ -889,7 +896,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 等积分层需原始标量:从当前体素 image 抽取(无则等积退化线性)。 // 大体素按步长抽样(等积分位无需全量点),避免主线程长循环卡 UI。 std::vector samples; - if (vtkImageData* img = sceneView->currentVolumeImage()) { + if (vtkImageData* img = vol->image.Get()) { if (vtkDataArray* sc = img->GetPointData()->GetScalars()) { const vtkIdType n = sc->GetNumberOfTuples(); if (n > 0) { @@ -907,8 +914,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 「另存为/打开」与「新建色阶/配色方案」走色阶模板仓储,projectId 取当前项目。 // 3D 体无来源 lvl 模板 → lvlTemplateId 传空(覆盖复选框禁用,行为不变)。 geopro::app::ColorScaleConfigDialog dlg( - sceneView->currentColorScale(), sceneView->currentVmin(), - sceneView->currentVmax(), std::move(samples), {}, &colorTplRepo, + vol->cs, vol->vmin, vol->vmax, std::move(samples), {}, &colorTplRepo, nav.currentProjectId(), QString(), &window); if (dlg.exec() == QDialog::Accepted) sceneCtrl->setVolumeColorScale(dsId, dlg.colorScale()); @@ -932,6 +938,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re interactionMgr->selectSavedSlice(id); // 选中已渲染的该切片(高亮) renderWindowPtr->Render(); }); + // 反向 VTK→list:在 VTK 里点中/选中一张切片 → 在三维体段树里同步选中该切片行(②反向)。 + interactionMgr->onSliceSelectionChanged = [drawer](const std::string& dsId) { + if (auto* sec = drawer->analysisTab()->section("voxel")) + sec->selectItem(QString::fromStdString(dsId)); + }; // 异常双击属性(R83)/右键删除已并入 analysisTab 的 detailRequested(dd_anomaly) / // deleteDatasetRequested(dd_anomaly);列表选中→VTK高亮(R84)随旧栏退役暂缺,待新段补 anomalySelected。 diff --git a/src/app/panels/columns/CategoryAnalysisTab.hpp b/src/app/panels/columns/CategoryAnalysisTab.hpp index f32a096..1f5afe5 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.hpp +++ b/src/app/panels/columns/CategoryAnalysisTab.hpp @@ -34,7 +34,7 @@ signals: void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除切片/异常 // ── 三维体段操作转发(迁自旧 Column3DAnalysis,全接)── - void sliceRequested(geopro::render::interact::SliceAxis axis); + void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); void colorScaleRequested(const QString& dsId); void sliceSaveRequested(const QString& dsId); void sliceSaveAsRequested(const QString& dsId); diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp index c1242c1..1e7a654 100644 --- a/src/app/panels/columns/CategorySection.cpp +++ b/src/app/panels/columns/CategorySection.cpp @@ -131,6 +131,16 @@ void CategorySection::setDatasets(const std::vector& rows) { rebuildList(); } +void CategorySection::selectItem(const QString& dsId) { + const QSignalBlocker block(list_); // 程序化选中(VTK→list)不回发 datasetSelected,避免环路 + for (QTreeWidgetItemIterator it(list_); *it; ++it) + if (!dsId.isEmpty() && (*it)->data(0, kDsIdRole).toString() == dsId) { + list_->setCurrentItem(*it); + return; + } + list_->setCurrentItem(nullptr); // 空 dsId / 未找到 → 清选中 +} + void CategorySection::setChecked(const QString& dsId, bool on) { for (QTreeWidgetItemIterator it(list_); *it; ++it) if ((*it)->data(0, kDsIdRole).toString() == dsId && @@ -294,11 +304,11 @@ void CategorySection::showContextMenu(const QPoint& pos) { menu.addAction(QStringLiteral("详情"), this, [this, id, ddCode, name] { emit detailRequested(id, ddCode, name); }); if (ddCode == QStringLiteral("dd_voxel")) { // 三维体 - QMenu* sl = menu.addMenu(QStringLiteral("生成切片")); - sl->addAction(QStringLiteral("上下"), this, [this] { emit sliceRequested(SliceAxis::UpDown); }); - sl->addAction(QStringLiteral("前后"), this, [this] { emit sliceRequested(SliceAxis::FrontBack); }); - sl->addAction(QStringLiteral("左右"), this, [this] { emit sliceRequested(SliceAxis::LeftRight); }); - sl->addAction(QStringLiteral("任意"), this, [this] { emit sliceRequested(SliceAxis::Oblique); }); + QMenu* sl = menu.addMenu(QStringLiteral("生成切片")); // id=被右键的三维体 dsId(切片建到该体上) + sl->addAction(QStringLiteral("上下"), this, [this, id] { emit sliceRequested(SliceAxis::UpDown, id); }); + sl->addAction(QStringLiteral("前后"), this, [this, id] { emit sliceRequested(SliceAxis::FrontBack, id); }); + sl->addAction(QStringLiteral("左右"), this, [this, id] { emit sliceRequested(SliceAxis::LeftRight, id); }); + sl->addAction(QStringLiteral("任意"), this, [this, id] { emit sliceRequested(SliceAxis::Oblique, id); }); menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); }); } else if (ddCode == QStringLiteral("dd_slice")) { // 切片 menu.addAction(QStringLiteral("保存位姿"), this, [this, id] { emit sliceSaveRequested(id); }); diff --git a/src/app/panels/columns/CategorySection.hpp b/src/app/panels/columns/CategorySection.hpp index c25ad18..70385cc 100644 --- a/src/app/panels/columns/CategorySection.hpp +++ b/src/app/panels/columns/CategorySection.hpp @@ -33,6 +33,7 @@ public: void setStructure(const std::vector& nodes); void setDatasets(const std::vector& rows); void setChecked(const QString& dsId, bool on); // 按 dsId 勾选/取消(新建切片自动勾选等场景) + void selectItem(const QString& dsId); // 程序化选中某行(VTK→list 反向联动);空/未找到=清选中 QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds(异常显隐同步用) void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉 const CategorySpec& spec() const { return spec_; } @@ -43,7 +44,7 @@ signals: void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情 void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常) // ── 三维体段右键操作(迁自旧 Column3DAnalysis,全接)── - void sliceRequested(geopro::render::interact::SliceAxis axis); // 体→生成切片(轴) + void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); // 体→生成切片(轴+目标体) void colorScaleRequested(const QString& dsId); // 体/切片→色阶 void sliceSaveRequested(const QString& dsId); // 切片→保存位姿 void sliceSaveAsRequested(const QString& dsId); // 切片→另存 diff --git a/src/render/interact/InteractionManager.cpp b/src/render/interact/InteractionManager.cpp index ed179df..dffece6 100644 --- a/src/render/interact/InteractionManager.cpp +++ b/src/render/interact/InteractionManager.cpp @@ -95,42 +95,82 @@ void InteractionManager::updateSelectionVisual() { slices_[i]->setSelected(static_cast(i) == selected_); } -void InteractionManager::setVolumeImage(vtkImageData* image, const geopro::core::ColorScale& cs, - double vmin, double vmax) { - // 体素重建/变更:先释放旧切片(旧 image 即将失效),再附着新 image。 - closeAll(); - image_ = image; - colorScale_ = cs; - vmin_ = vmin; - vmax_ = vmax; +const InteractionManager::VolumeImg* InteractionManager::volumeOf(const std::string& volumeDsId) const { + auto it = volumes_.find(volumeDsId); + return it != volumes_.end() ? &it->second : nullptr; } -void InteractionManager::addSlice(SliceAxis axis) { - if (!image_ || !interactor_) return; - auto tool = std::make_unique(image_, interactor_, axis, colorScale_, vmin_, vmax_); +vtkImageData* InteractionManager::selectedVolumeImage() const { + if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; + const VolumeImg* v = volumeOf(slices_[static_cast(selected_)]->volumeDsId()); + return v ? v->image : nullptr; +} + +void InteractionManager::setVolumeImage(const std::string& volumeDsId, vtkImageData* image, + const geopro::core::ColorScale& cs, double vmin, double vmax) { + if (volumeDsId.empty()) return; + auto it = volumes_.find(volumeDsId); + // 同体 image 变更(重建/改色阶):旧 image 即将失效 → 先关该体已显示切片(上层 syncSlices 用新 image 重现)。 + if (it != volumes_.end() && it->second.image != image) closeSlicesOfVolume(volumeDsId); + volumes_[volumeDsId] = VolumeImg{image, cs, vmin, vmax}; +} + +void InteractionManager::removeVolumeImage(const std::string& volumeDsId) { + if (!volumes_.count(volumeDsId)) return; + closeSlicesOfVolume(volumeDsId); // 体取消渲染 → 关其下所有切片 + volumes_.erase(volumeDsId); +} + +std::vector InteractionManager::volumeIds() const { + std::vector ids; + ids.reserve(volumes_.size()); + for (const auto& kv : volumes_) ids.push_back(kv.first); + return ids; +} + +void InteractionManager::closeSlicesOfVolume(const std::string& volumeDsId) { + for (std::size_t i = slices_.size(); i-- > 0;) { + if (slices_[i]->volumeDsId() != volumeDsId) 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(); +} + +void InteractionManager::addSlice(SliceAxis axis, const std::string& volumeDsId) { + const VolumeImg* v = volumeOf(volumeDsId); + if (!v || !v->image || !interactor_) return; + auto tool = std::make_unique(v->image, interactor_, axis, v->cs, v->vmin, v->vmax); + tool->setVolumeDsId(volumeDsId); // 触碰本切片(拖动/点击切面) → 设为选中(widget 开启交互后独占切面事件,选中靠此回调)。 SliceTool* tp = tool.get(); tool->onInteract = [this, tp]() { selectByTool(tp); }; slices_.push_back(std::move(tool)); selected_ = static_cast(slices_.size()) - 1; // 新切片选中 updateSelectionVisual(); + if (onSliceSelectionChanged) onSliceSelectionChanged(std::string{}); // 新建(未保存)切片→清列表选中 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; + const Vec3& point1, const Vec3& point2, + const std::string& volumeDsId) { + const VolumeImg* v = volumeOf(volumeDsId); + if (!v || !v->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_, + auto tool = std::make_unique(v->image, interactor_, ax, v->cs, v->vmin, v->vmax, origin, point1, point2); // 三点精确还原 tool->setDsId(dsId); + tool->setVolumeDsId(volumeDsId); SliceTool* tp = tool.get(); tool->onInteract = [this, tp]() { selectByTool(tp); }; slices_.push_back(std::move(tool)); selected_ = static_cast(slices_.size()) - 1; - updateSelectionVisual(); + updateSelectionVisual(); // 程序化显示(syncSlices):不发 onSliceSelectionChanged,避免列表选中被刷 safeRender(); } @@ -172,6 +212,8 @@ void InteractionManager::selectByTool(const SliceTool* tool) { if (idx < 0) return; selected_ = idx; updateSelectionVisual(); + if (onSliceSelectionChanged) // 反向 VTK→list:选中切片 → 列表同步选中(dsId 空=临时切片) + onSliceSelectionChanged(slices_[static_cast(idx)]->dsId()); // 双击切片正视(D40):同一切片在 350ms 内两次交互 → 视为双击 → 正视。 const double now = std::chrono::duration( @@ -239,6 +281,11 @@ std::string InteractionManager::selectedSliceDsId() const { return slices_[static_cast(selected_)]->dsId(); } +std::string InteractionManager::selectedSliceVolumeDsId() const { + if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return {}; + return slices_[static_cast(selected_)]->volumeDsId(); +} + void InteractionManager::tagSelectedSlice(const std::string& dsId) { if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return; slices_[static_cast(selected_)]->setDsId(dsId); @@ -269,8 +316,11 @@ vtkSmartPointer InteractionManager::selectedSliceColorImage() cons std::max(1, static_cast(ny * f)), 1); resize->Update(); - // 用与切片显示同一色阶 LUT 上色(colorScale_/vmin_/vmax_ 即当前体/切片着色区间)。 - auto lut = buildLut(colorScale_, vmin_, vmax_); + // 用与切片显示同一色阶 LUT 上色:取选中切片所属体的色阶(多体并发各体色阶不同)。 + const VolumeImg* v = (selected_ >= 0 && selected_ < static_cast(slices_.size())) + ? volumeOf(slices_[static_cast(selected_)]->volumeDsId()) + : nullptr; + auto lut = v ? buildLut(v->cs, v->vmin, v->vmax) : buildLut(geopro::core::ColorScale{}, 0.0, 1.0); vtkNew map; map->SetInputConnection(resize->GetOutputPort()); map->SetLookupTable(lut); @@ -321,7 +371,9 @@ int InteractionManager::nearestSlice(const Vec3& worldPoint) const { const int idx = nearestPlane(centers, normals, worldPoint); if (idx < 0) return -1; // 阈值:命中点离最近切面太远(> 体对角线 5%)视为"没点在切片上",不改选中(评审 M2)。 - const std::array b = imageBounds(image_); + // 多体并发:用该切片所属体的包围盒(各体大小不同)。 + const VolumeImg* vol = volumeOf(slices_[static_cast(idx)]->volumeDsId()); + const std::array b = imageBounds(vol ? vol->image : nullptr); const double dx = b[1] - b[0], dy = b[3] - b[2], dz = b[5] - b[4]; const double diag = std::sqrt(dx * dx + dy * dy + dz * dz); const double dist = slices_[static_cast(idx)]->distanceToPlane(worldPoint); @@ -334,6 +386,10 @@ void InteractionManager::onPicked(const Vec3& worldPoint) { // 解决"选了切片无法取消":点击切片之外即清选中,滚轮恢复缩放(见 onWheel)。 selected_ = nearestSlice(worldPoint); updateSelectionVisual(); + if (onSliceSelectionChanged) // 反向 VTK→list:点中切片→列表同步选中;点空(idx<0)→清列表选中 + onSliceSelectionChanged(selected_ >= 0 + ? slices_[static_cast(selected_)]->dsId() + : std::string{}); safeRender(); } @@ -368,7 +424,7 @@ bool InteractionManager::onWheel(int dir) { // 配合 onPicked 的"点击切片外取消选中":取消后滚轮即恢复缩放,解决"选了切片无法缩放"。 // (不采用"悬停即推进":推进时鼠标难持续压在移动的切片上,且过敏感。) if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return false; - const double step = wheelStep(imageBounds(image_), dir); + const double step = wheelStep(imageBounds(selectedVolumeImage()), dir); // 选中切片所属体 slices_[static_cast(selected_)]->advance(step); safeRender(); return true; // 消费滚轮(推进选中切片,不缩放) diff --git a/src/render/interact/InteractionManager.hpp b/src/render/interact/InteractionManager.hpp index a35b7b0..c350d7b 100644 --- a/src/render/interact/InteractionManager.hpp +++ b/src/render/interact/InteractionManager.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -38,19 +39,23 @@ public: InteractionManager(const InteractionManager&) = delete; InteractionManager& operator=(const InteractionManager&) = delete; - // 设置当前体素 image + 色阶(体素重建后调;image 变更先 closeAll 再附着新 image)。 - // image=nullptr → 清空附着,切片创建无效。 - void setVolumeImage(vtkImageData* image, const geopro::core::ColorScale& cs, double vmin, - double vmax); + // 新增/更新某三维体的 image + 色阶(多体并发:不影响其它体的切片)。同体 image 变更(重建/改色阶) + // 会先关该体已显示切片(旧 image 失效),由上层 syncSlices 用新 image 重现。 + void setVolumeImage(const std::string& volumeDsId, vtkImageData* image, + const geopro::core::ColorScale& cs, double vmin, double vmax); + // 移除某体(体被取消渲染):关闭其下所有切片并去除该体 image。 + void removeVolumeImage(const std::string& volumeDsId); + // 当前已附着的三维体 dsId 列表(上层据此 diff 出需移除的体)。 + std::vector volumeIds() const; - // 创建一张切片(轴向/任意)。无体素 image 则忽略。新切片自动设为选中。 - void addSlice(SliceAxis axis); + // 在指定三维体上创建一张切片(轴向/任意)。该体无 image 则忽略。新切片自动设为选中。 + void addSlice(SliceAxis axis, const std::string& volumeDsId); // ── 已保存切片(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); + const Vec3& point2, const std::string& volumeDsId); void hideSavedSlice(const std::string& dsId); std::vector shownSavedSliceIds() const; // 当前已显示的已保存切片 dsId 列表 // 选中已显示的某 dsId 切片(列表操作定位到对应渲染切片);找到返回 true。 @@ -60,8 +65,11 @@ public: void closeSelected(); // 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。 void closeAll(); + // 关闭并释放某体下的所有切片(该体移除/重建时;不动其它体的切片)。 + void closeSlicesOfVolume(const std::string& volumeDsId); - bool hasVolume() const { return image_ != nullptr; } + bool hasVolume() const { return !volumes_.empty(); } + bool isVolumeRendered(const std::string& volumeDsId) const { return volumes_.count(volumeDsId) > 0; } bool hasSlices() const { return !slices_.empty(); } int sliceCount() const { return static_cast(slices_.size()); } @@ -74,6 +82,8 @@ public: bool selectedSlicePlane(int& axis, Vec3& origin, Vec3& point1, Vec3& point2) const; // 选中切片的归属 dsId(已保存切片非空;未保存为空)。无选中返回空字符串。 std::string selectedSliceDsId() const; + // 选中切片所属三维体 dsId(保存切片/创建异常时定位到正确的体)。无选中返回空字符串。 + std::string selectedSliceVolumeDsId() const; // 给当前选中(未保存)切片打 dsId 标签:保存=把当前切片链接到新数据集(不重绘、不重复)。 void tagSelectedSlice(const std::string& dsId); // 选中切片的重采样 2D 标量影像(导出 dat 用);无选中返回 nullptr。 @@ -88,6 +98,10 @@ public: // 仅 closeSelected(用户主动关闭) 触发;closeAll(体变更/清场) 不触发(切片应随体回来再现)。 std::function onSliceClosed; + // 选中切片变化(VTK 内拾取/widget 交互选中一张已保存切片)→ 回调其 dsId,供上层在列表里同步选中 + // (反向 VTK→list 联动)。选中临时(未保存)切片或取消选中(空 dsId)也会回调。 + std::function onSliceSelectionChanged; + // 安装/卸载自定义交互样式(构造时安装;析构卸载恢复原样式)。 void installStyle(); void uninstallStyle(); @@ -121,9 +135,17 @@ private: vtkRenderWindow* renderWindow_; vtkRenderer* renderer_; - vtkImageData* image_ = nullptr; // 非拥有;当前体素 image - geopro::core::ColorScale colorScale_; - double vmin_ = 0.0, vmax_ = 0.0; + // 多体并发:按三维体 dsId 持各体的 image + 色阶(切片附着到各自体的 image,互不影响)。 + struct VolumeImg { + vtkImageData* image = nullptr; // 非拥有 + geopro::core::ColorScale cs; + double vmin = 0.0, vmax = 0.0; + }; + std::map volumes_; // 三维体 dsId → image/色阶 + // 取某体 image(缺则 nullptr)。 + const VolumeImg* volumeOf(const std::string& volumeDsId) const; + // 选中切片所属体的 image(nearestSlice 阈值/滚轮步长用);无选中或体缺则 nullptr。 + vtkImageData* selectedVolumeImage() const; std::vector> slices_; int selected_ = -1; // 选中切片索引(-1=无) diff --git a/src/render/interact/SliceTool.hpp b/src/render/interact/SliceTool.hpp index d5d8189..8075566 100644 --- a/src/render/interact/SliceTool.hpp +++ b/src/render/interact/SliceTool.hpp @@ -50,6 +50,10 @@ public: const std::string& dsId() const { return dsId_; } void setDsId(std::string id) { dsId_ = std::move(id); } + // 本切片所属三维体 dsId(多体并发:每张切片附着到各自体的 image;用于按体定位/移除/取色阶)。 + const std::string& volumeDsId() const { return volumeDsId_; } + void setVolumeDsId(std::string id) { volumeDsId_ = std::move(id); } + // 取当前切面精确三点(保存用)。 void planePoints(double origin[3], double point1[3], double point2[3]) const; @@ -79,7 +83,8 @@ public: private: SliceAxis axis_; - std::string dsId_; // 已保存切片归属标签(空=临时交互切片) + std::string dsId_; // 已保存切片归属标签(空=临时交互切片) + std::string volumeDsId_; // 所属三维体 dsId(多体并发用) vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证 // 把已存在的 image 接入 widget 的 producer:须随 SliceTool 保活(否则构造后析构→管线断裂崩溃,评审 H1)。 vtkSmartPointer producer_;