feat(vtk): 多三维体并发切片渲染重构(OPT-002:issue2+③+反向②)

核心: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 测试通过
This commit is contained in:
gaozheng 2026-06-25 20:36:43 +08:00
parent 85ae48ebfb
commit 69e8790810
9 changed files with 206 additions and 71 deletions

View File

@ -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 的体素被移除 → 切片源失效
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 (onVolumeChanged) onVolumeChanged();
}
}
if (wasVolume && onVolumeChanged) onVolumeChanged(); // 任一体移除 → 上层多体同步切片
}
void VtkSceneView::addAnomaly(const geopro::core::Anomaly& a) {

View File

@ -116,11 +116,29 @@ private:
// 持引用以便重建前移除旧 prop避免叠加评审 HIGH
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
// 当前体素 image + 色阶P3 切片附着源);无体素时为空。
// 当前体素 image + 色阶P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/
// 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。
vtkSmartPointer<vtkImageData> currentVolumeImage_;
geopro::core::ColorScale currentColorScale_;
double currentVmin_ = 0.0;
double currentVmax_ = 0.0;
// 多体并发:按 dsId 持各已渲染体的 image + 色阶(供 InteractionManager 让各体切片各用自己的 image
struct VolumeRec {
vtkSmartPointer<vtkImageData> image;
geopro::core::ColorScale cs;
double vmin = 0.0, vmax = 0.0;
};
std::map<std::string, VolumeRec> volumes_;
public:
const std::map<std::string, VolumeRec>& 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 自管、不在此。

View File

@ -409,22 +409,20 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 勾选且父体=当前体 → 显示(按 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();
// 移除:已显示但不再需要(未勾选 / 父体非当前体 / 无活动体)。
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);
}
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<double> 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。

View File

@ -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);

View File

@ -131,6 +131,16 @@ void CategorySection::setDatasets(const std::vector<DsRow>& 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); });

View File

@ -33,6 +33,7 @@ public:
void setStructure(const std::vector<geopro::data::StructNode>& nodes);
void setDatasets(const std::vector<geopro::data::DsRow>& 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); // 切片→另存

View File

@ -95,42 +95,82 @@ void InteractionManager::updateSelectionVisual() {
slices_[i]->setSelected(static_cast<int>(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<SliceTool>(image_, interactor_, axis, colorScale_, vmin_, vmax_);
vtkImageData* InteractionManager::selectedVolumeImage() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
const VolumeImg* v = volumeOf(slices_[static_cast<std::size_t>(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<std::string> InteractionManager::volumeIds() const {
std::vector<std::string> 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<long>(i));
}
selected_ = slices_.empty() ? -1 : std::min(selected_, static_cast<int>(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<SliceTool>(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<int>(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<SliceAxis>(axis);
auto tool = std::make_unique<SliceTool>(image_, interactor_, ax, colorScale_, vmin_, vmax_,
auto tool = std::make_unique<SliceTool>(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<int>(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<std::size_t>(idx)]->dsId());
// 双击切片正视(D40):同一切片在 350ms 内两次交互 → 视为双击 → 正视。
const double now = std::chrono::duration<double, std::milli>(
@ -239,6 +281,11 @@ std::string InteractionManager::selectedSliceDsId() const {
return slices_[static_cast<std::size_t>(selected_)]->dsId();
}
std::string InteractionManager::selectedSliceVolumeDsId() const {
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return {};
return slices_[static_cast<std::size_t>(selected_)]->volumeDsId();
}
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);
@ -269,8 +316,11 @@ vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() cons
std::max(1, static_cast<int>(ny * f)), 1);
resize->Update();
// 用与切片显示同一色阶 LUT 上色colorScale_/vmin_/vmax_ 即当前体/切片着色区间)。
auto lut = buildLut(colorScale_, vmin_, vmax_);
// 用与切片显示同一色阶 LUT 上色:取选中切片所属体的色阶(多体并发各体色阶不同)。
const VolumeImg* v = (selected_ >= 0 && selected_ < static_cast<int>(slices_.size()))
? volumeOf(slices_[static_cast<std::size_t>(selected_)]->volumeDsId())
: nullptr;
auto lut = v ? buildLut(v->cs, v->vmin, v->vmax) : buildLut(geopro::core::ColorScale{}, 0.0, 1.0);
vtkNew<vtkImageMapToColors> 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<double, 6> b = imageBounds(image_);
// 多体并发:用该切片所属体的包围盒(各体大小不同)。
const VolumeImg* vol = volumeOf(slices_[static_cast<std::size_t>(idx)]->volumeDsId());
const std::array<double, 6> 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<std::size_t>(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<std::size_t>(selected_)]->dsId()
: std::string{});
safeRender();
}
@ -368,7 +424,7 @@ bool InteractionManager::onWheel(int dir) {
// 配合 onPicked 的"点击切片外取消选中":取消后滚轮即恢复缩放,解决"选了切片无法缩放"。
// (不采用"悬停即推进":推进时鼠标难持续压在移动的切片上,且过敏感。)
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
const double step = wheelStep(imageBounds(image_), dir);
const double step = wheelStep(imageBounds(selectedVolumeImage()), dir); // 选中切片所属体
slices_[static_cast<std::size_t>(selected_)]->advance(step);
safeRender();
return true; // 消费滚轮(推进选中切片,不缩放)

View File

@ -1,5 +1,6 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
@ -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<std::string> 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<std::string> 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<int>(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<void(const std::string& dsId)> onSliceClosed;
// 选中切片变化VTK 内拾取/widget 交互选中一张已保存切片)→ 回调其 dsId供上层在列表里同步选中
// (反向 VTK→list 联动)。选中临时(未保存)切片或取消选中(空 dsId)也会回调。
std::function<void(const std::string& dsId)> 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<std::string, VolumeImg> volumes_; // 三维体 dsId → image/色阶
// 取某体 image缺则 nullptr
const VolumeImg* volumeOf(const std::string& volumeDsId) const;
// 选中切片所属体的 imagenearestSlice 阈值/滚轮步长用);无选中或体缺则 nullptr。
vtkImageData* selectedVolumeImage() const;
std::vector<std::unique_ptr<SliceTool>> slices_;
int selected_ = -1; // 选中切片索引(-1=无)

View File

@ -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;
@ -80,6 +84,7 @@ public:
private:
SliceAxis axis_;
std::string dsId_; // 已保存切片归属标签(空=临时交互切片)
std::string volumeDsId_; // 所属三维体 dsId多体并发用
vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证
// 把已存在的 image 接入 widget 的 producer须随 SliceTool 保活(否则构造后析构→管线断裂崩溃,评审 H1
vtkSmartPointer<vtkTrivialProducer> producer_;