feat(vtk): 切片右键菜单(VTK视图)+切片持久化mock+导出图片(切片本身,高清)/dat (#3a)
- VTK视图切片右键菜单(设计§2.3):正视图/翻转/关闭(接现有交互)、导出图片/导出dat、保存、创建异常(占位→#4)
- 右键经 InteractionManager 高优先级(1.0)交互器观察者实现:先于 vtkImagePlaneWidget 消费右键
(widget 会抢右键并 abort,故 InteractorStyle::OnRightButtonDown 不触发);命中切片或回退选中切片→弹菜单
- 切片持久化 mock:Api3dRepository createSlice/saveSlice/deleteSlice 内存态 + sliceRows()
(ddCode=dd_slice,parentId=所属体→三维分析栏树中挂父体下,符合需求 R19 对象/三维体/切片结构)
- 导出为图片=导出切片本身(需求 R52):切片重采样 2D → 上采样至最长边2048(保长宽比) → 切片色阶LUT上色 → PNG
(非整窗截图);导出dat=切片重采样标量网格
- 三维分析栏接线(#5部分):显示/隐藏(VtkSceneView::toggleDatasetVisibility)、切片删除、色阶占位
- main.cpp refreshAnalysis 合并 volumeRows+sliceRows 注入三维分析栏
编译链接绿(build.bat app exit 0);右键菜单/保存/导出图片(切片·高清)已用户实测通过。
3b 待做:已保存切片在分析栏勾选后的重渲染(从 spec 重建)、分析栏保存/另存/导出;#4 异常接真实后端。
This commit is contained in:
parent
b261374cc9
commit
afdd98f416
|
|
@ -50,10 +50,11 @@
|
|||
|
||||
**实现拆解(设计文档 §6,按依赖排序)**:
|
||||
1. ~~三维体 mock 渲染~~ **✅ 已实现(编译绿,待 GUI 实测)**——见 §3 与计划 `2026-06-17-vtk-3d-volume-create-flow.md`。`Api3dRepository::loadVolume` 已接通(多源复用 loadSection → IDW → VolumeGrid + 色阶交付);`VolumeBuildParams` 必存参数、values 惰性重算+缓存(**不冻结 gridSpec**,改用源 ds 锁定不变式,留校验 TODO)。
|
||||
2. 切片交互接通三维体(现有 `SliceTool`/`InteractionManager` 已能切;补滚轮推进、双击正视)。
|
||||
3. 切片保存/另存/导出/删除(保存删除 mock 内存;导出图片/dat 客户端做)+ VTK 视图切片右键菜单接线。
|
||||
4. 异常:切片右键创建异常(圈定+保存对话框含截图)→ **接真实端点**。
|
||||
5. 分析栏右键菜单接线:色阶/显示隐藏(客户端)+ 切片增删(接 #3)。`Column3DAnalysis` 信号已定义,main.cpp 目前**只接了 `sliceRequested`+`detailRequested`**,其余未连。
|
||||
2. ~~切片交互接通三维体~~ **✅ 已有**(`SliceTool`/`InteractionManager`:四向创建/滚轮推进/双击正视/翻转/关闭/选中高亮全在)。
|
||||
3. **✅ 已实现(3a,编译绿,未提交,待 GUI 实测)**——VTK 视图切片右键菜单(`PickInteractorStyle` 右键→`InteractionManager::onSliceContextMenuRequested`→main.cpp 弹 QMenu):正视图/翻转/关闭(接现有)、导出图片(PNG)/导出dat(`SliceExport.{hpp,cpp}`)、**保存**(`Api3dRepository::createSlice` 内存 mock→dd_slice 行进三维分析栏、挂父体下,`sliceRows()`+`refreshAnalysis` 合并)、创建异常(占位→#4)。`deleteSlice` 亦改 mock。
|
||||
- **3b 待做(拆出)**:已保存切片在三维分析栏勾选后的**重渲染**(切片现仅为交互 widget,从 spec 重建需重构面内两轴 + 与父体加载排序 + 跨层编排);分析栏的保存/另存/导出(依赖重渲染)。
|
||||
4. 异常:切片右键创建异常(圈定+保存对话框含截图)→ **接真实端点**。VTK 切片右键菜单的「创建异常」入口已占位。
|
||||
5. 分析栏右键接线:**部分完成**——`visibilityToggled`(显隐,`VtkSceneView::toggleDatasetVisibility`)、`sliceDeleteRequested`(删 mock 切片+刷新) 已接;`colorScaleRequested` 占位;`sliceSave/SaveAs/Export`(分析栏入口) 待 3b。
|
||||
6. 三维体/切片/异常详情面板(源数据/插值参数/色阶/测量点数体积/异常列表)。
|
||||
|
||||
**其它小项**:坐标轴「O点位置」「字体」弹框仍是 stub(main.cpp:382 TODO P4)。
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ find_package(VTK REQUIRED COMPONENTS
|
|||
InteractionWidgets
|
||||
FiltersGeometry
|
||||
FiltersModeling
|
||||
IOImage # vtkPNGWriter(切片导出图片)
|
||||
)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Svg)
|
||||
|
|
@ -64,6 +65,7 @@ add_executable(geopro_desktop WIN32
|
|||
ImportDatasetDialog.cpp
|
||||
ExportDatasetDialog.cpp
|
||||
SettingsDialog.cpp
|
||||
SliceExport.cpp
|
||||
VolumeParamsDialog.cpp
|
||||
Logging.cpp
|
||||
DatasetDimension.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
#include "SliceExport.hpp"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <vtkDataArray.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPNGWriter.h>
|
||||
#include <vtkPointData.h>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path) {
|
||||
if (colorImage == nullptr || path.empty()) return false;
|
||||
vtkNew<vtkPNGWriter> writer;
|
||||
writer->SetFileName(path.c_str());
|
||||
writer->SetInputData(colorImage); // 已上色 RGB 的切片 2D 图(非整窗截图)
|
||||
writer->Write();
|
||||
return writer->GetErrorCode() == 0;
|
||||
}
|
||||
|
||||
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
|
||||
if (slice == nullptr || path.empty()) return false;
|
||||
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
||||
if (arr == nullptr) return false;
|
||||
int dims[3];
|
||||
slice->GetDimensions(dims);
|
||||
const int nx = dims[0], ny = dims[1];
|
||||
if (nx < 1 || ny < 1) return false;
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out) return false;
|
||||
// 切片重采样为 2D(dims[2]=1):写成行=j、列=i 的标量网格,每格取首分量。
|
||||
for (int j = 0; j < ny; ++j) {
|
||||
for (int i = 0; i < nx; ++i) {
|
||||
const vtkIdType id = static_cast<vtkIdType>(j) * nx + i;
|
||||
out << arr->GetComponent(id, 0) << (i + 1 < nx ? ' ' : '\n');
|
||||
}
|
||||
}
|
||||
return static_cast<bool>(out);
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class vtkImageData;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 把切片"上色后"的 2D RGB 影像写为 PNG(切片右键「导出为图片」= 导出切片本身,非整窗截图)。
|
||||
bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path);
|
||||
|
||||
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。
|
||||
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -189,6 +189,15 @@ 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,6 +39,8 @@ 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;
|
||||
|
|
@ -54,6 +56,7 @@ public:
|
|||
double currentVmin() const { return currentVmin_; }
|
||||
double currentVmax() const { return currentVmax_; }
|
||||
bool hasVolume() const { return currentVolumeImage_ != nullptr; }
|
||||
const std::string& currentVolumeDsId() const { return volumeOwnerDs_; } // 当前体归属 ds(保存切片用)
|
||||
|
||||
// 体素 image 变化(addVolume 附着新 image / clear 置空)时回调,供上层把新 image 推给
|
||||
// InteractionManager(重附着或关闭切片)。clear 时以 nullptr 触发。
|
||||
|
|
|
|||
112
src/app/main.cpp
112
src/app/main.cpp
|
|
@ -41,7 +41,12 @@
|
|||
#include <QSlider>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QDate>
|
||||
#include <QAction>
|
||||
#include <QCursor>
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -92,6 +97,7 @@
|
|||
#include "PanelHeader.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "SettingsDialog.hpp"
|
||||
#include "SliceExport.hpp"
|
||||
#include "TopBar.hpp"
|
||||
#include "VolumeParamsDialog.hpp"
|
||||
#include "ProjectListDialog.hpp"
|
||||
|
|
@ -155,6 +161,7 @@
|
|||
#include <vtkGenericOpenGLRenderWindow.h>
|
||||
#include <vtkLookupTable.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkRenderWindowInteractor.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
|
@ -369,7 +376,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
auto lastAnalysisRows = std::make_shared<std::vector<geopro::data::DsRow>>();
|
||||
auto refreshAnalysis = [drawer, scene3dRepo, lastAnalysisRows]() {
|
||||
std::vector<geopro::data::DsRow> rows = *lastAnalysisRows;
|
||||
for (auto& vr : scene3dRepo->volumeRows()) rows.push_back(std::move(vr));
|
||||
for (auto& vr : scene3dRepo->volumeRows()) rows.push_back(std::move(vr)); // 客户端三维体
|
||||
for (auto& sr : scene3dRepo->sliceRows()) rows.push_back(std::move(sr)); // 已保存切片(挂父体下)
|
||||
drawer->colAnalysis()->setDatasets(rows);
|
||||
};
|
||||
|
||||
|
|
@ -383,6 +391,90 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
sceneCtrl->setCheckedDatasets(all);
|
||||
};
|
||||
|
||||
// ── VTK 视图切片右键菜单(设计 §2.3)──────────────────────────────────────
|
||||
// 右键命中切片 → InteractionManager 选中并回调本 lambda → 弹菜单(QCursor 处定位)。
|
||||
// 正视/翻转/关闭=接现有交互;保存=持久化为 dd_slice 进三维分析栏;导出图片/dat=客户端;
|
||||
// 创建异常=占位(#4 接真实后端)。
|
||||
interactionMgr->onSliceContextMenuRequested =
|
||||
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis]() {
|
||||
QMenu menu(&window);
|
||||
QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常"));
|
||||
QAction* aSave = menu.addAction(QStringLiteral("保存"));
|
||||
QAction* aImg = menu.addAction(QStringLiteral("导出为图片"));
|
||||
QAction* aDat = menu.addAction(QStringLiteral("导出到 dat"));
|
||||
menu.addSeparator();
|
||||
QAction* aFace = menu.addAction(QStringLiteral("正视图"));
|
||||
QAction* aFlip = menu.addAction(QStringLiteral("视图翻转"));
|
||||
QAction* aClose = menu.addAction(QStringLiteral("关闭"));
|
||||
|
||||
QAction* chosen = menu.exec(QCursor::pos());
|
||||
if (chosen == nullptr) return;
|
||||
if (chosen == aFace) { interactionMgr->faceSelected(); return; }
|
||||
if (chosen == aFlip) { interactionMgr->flipView(); return; }
|
||||
if (chosen == aClose) { interactionMgr->closeSelected(); return; }
|
||||
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()) {
|
||||
QMessageBox::warning(&window, QStringLiteral("保存切片"),
|
||||
QStringLiteral("当前切片无所属三维体,无法保存。"));
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(&window, QStringLiteral("保存切片"),
|
||||
QStringLiteral("切片名称"),
|
||||
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(); }, // 新切片进三维分析栏
|
||||
[&window](const std::string& m) {
|
||||
QMessageBox::warning(&window, QStringLiteral("保存切片"),
|
||||
QString::fromStdString(m));
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (chosen == aImg) {
|
||||
vtkSmartPointer<vtkImageData> colorImg = interactionMgr->selectedSliceColorImage();
|
||||
if (colorImg == nullptr) {
|
||||
QMessageBox::warning(&window, QStringLiteral("导出为图片"),
|
||||
QStringLiteral("无选中切片或切片无数据。"));
|
||||
return;
|
||||
}
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
&window, QStringLiteral("导出为图片"), QStringLiteral("slice.png"),
|
||||
QStringLiteral("PNG 图片 (*.png)"));
|
||||
if (!path.isEmpty() &&
|
||||
!geopro::app::exportSliceImagePng(colorImg, path.toStdString()))
|
||||
QMessageBox::warning(&window, QStringLiteral("导出为图片"),
|
||||
QStringLiteral("导出失败。"));
|
||||
return;
|
||||
}
|
||||
if (chosen == aDat) {
|
||||
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("导出到 dat"),
|
||||
QStringLiteral("导出失败。"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl,
|
||||
&geopro::controller::VtkSceneController::setAxesMode);
|
||||
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
|
||||
|
|
@ -438,6 +530,24 @@ 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 切片 + 刷新列表。
|
||||
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceDeleteRequested, &window,
|
||||
[scene3dRepo, refreshAnalysis](const QString& dsId) {
|
||||
scene3dRepo->deleteSlice(
|
||||
dsId.toStdString(), [refreshAnalysis]() { refreshAnalysis(); },
|
||||
[](const std::string&) {});
|
||||
});
|
||||
// 色阶 / 切片保存·另存·导出(分析栏入口):本期占位(色阶编辑、已存切片重渲染见 3b/后续)。
|
||||
QObject::connect(ca, &geopro::app::Column3DAnalysis::colorScaleRequested, &window,
|
||||
[&window](const QString&) {
|
||||
QMessageBox::information(&window, QStringLiteral("色阶"),
|
||||
QStringLiteral("色阶设置开发中。"));
|
||||
});
|
||||
|
||||
// ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token,经同一共享 GeoLocalFrame 配准)──
|
||||
auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window);
|
||||
|
|
|
|||
|
|
@ -204,21 +204,41 @@ void Api3dRepository::loadTerrainPaths(std::function<void(TerrainPaths)> /*onOk*
|
|||
onErr(kNotReady); // 后端地形 DEM/影像端点未就绪
|
||||
}
|
||||
|
||||
// ── 切片 CRUD(后端未就绪 → 变更走 onErr,给用户明确"未实现")──────────────
|
||||
// ── 切片 CRUD(后端无切片端点 → 内存 mock;端点就绪后换实现)────────────────
|
||||
|
||||
void Api3dRepository::createSlice(const SliceSpec& /*spec*/, const std::string& /*name*/,
|
||||
std::function<void(std::string)> /*onOk*/, OnError onErr) {
|
||||
onErr(kNotReady);
|
||||
std::vector<DsRow> Api3dRepository::sliceRows() const {
|
||||
std::vector<DsRow> rows;
|
||||
rows.reserve(slices_.size());
|
||||
for (const auto& [id, ss] : slices_) {
|
||||
DsRow r;
|
||||
r.id = id;
|
||||
r.dsName = ss.name;
|
||||
r.ddCode = "dd_slice";
|
||||
r.typeName = "切片";
|
||||
r.parentId = ss.spec.volumeDsId; // 树中挂在所属三维体下
|
||||
rows.push_back(std::move(r));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
void Api3dRepository::saveSlice(const std::string& /*dsId*/, const SliceSpec& /*spec*/,
|
||||
std::function<void()> /*onOk*/, OnError onErr) {
|
||||
onErr(kNotReady);
|
||||
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_);
|
||||
slices_[id] = StoredSlice{spec, name};
|
||||
onOk(id);
|
||||
}
|
||||
|
||||
void Api3dRepository::deleteSlice(const std::string& /*dsId*/, std::function<void()> /*onOk*/,
|
||||
OnError onErr) {
|
||||
onErr(kNotReady);
|
||||
void Api3dRepository::saveSlice(const std::string& dsId, const SliceSpec& spec,
|
||||
std::function<void()> onOk, OnError /*onErr*/) {
|
||||
auto it = slices_.find(dsId);
|
||||
if (it != slices_.end()) it->second.spec = spec; // 覆盖位姿
|
||||
onOk();
|
||||
}
|
||||
|
||||
void Api3dRepository::deleteSlice(const std::string& dsId, std::function<void()> onOk,
|
||||
OnError /*onErr*/) {
|
||||
slices_.erase(dsId);
|
||||
onOk();
|
||||
}
|
||||
|
||||
// ── 异常 / 异常体(load 回空树避免 UI 崩;变更走 onErr)─────────────────────
|
||||
|
|
|
|||
|
|
@ -38,8 +38,10 @@ public:
|
|||
// ── 客户端创建三维体(mock 持久化:内存;端点就绪后换实现)──────────────────
|
||||
// 登记新三维体(仅存参数,不立即插值)→ 返回新 dsId("vol-N")。插值在首次 loadVolume 惰性做并缓存。
|
||||
std::string createVolume(VolumeBuildParams params, const std::string& name);
|
||||
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维数据集栏合并注入(每次 setDatasets 追加)。
|
||||
// 已创建三维体的列表行(ddCode="dd_voxel"),供三维分析栏合并注入(每次 setDatasets 追加)。
|
||||
std::vector<DsRow> volumeRows() const;
|
||||
// 已保存切片的列表行(ddCode="dd_slice",parentId=所属体 dsId → 树中挂父体下),供三维分析栏合并。
|
||||
std::vector<DsRow> sliceRows() const;
|
||||
|
||||
void loadVolume(const std::string& dsId,
|
||||
std::function<void(VolumeGrid, geopro::core::ColorScale)> onOk,
|
||||
|
|
@ -95,6 +97,14 @@ private:
|
|||
};
|
||||
std::map<std::string, StoredVolume> volumes_; // dsId → 体
|
||||
int volumeCounter_ = 0;
|
||||
|
||||
// 内存态切片存储(mock;重启清空)。切片保存后成 dd_slice 数据集,进三维分析栏。
|
||||
struct StoredSlice {
|
||||
SliceSpec spec;
|
||||
std::string name;
|
||||
};
|
||||
std::map<std::string, StoredSlice> slices_; // dsId → 切片
|
||||
int sliceCounter_ = 0;
|
||||
};
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
|
|||
|
|
@ -5,12 +5,20 @@
|
|||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
#include <vtkCallbackCommand.h>
|
||||
#include <vtkCamera.h>
|
||||
#include <vtkCellPicker.h>
|
||||
#include <vtkCommand.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkImageMapToColors.h>
|
||||
#include <vtkImageResize.h>
|
||||
#include <vtkLookupTable.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderWindowInteractor.h>
|
||||
#include <vtkRenderer.h>
|
||||
|
||||
#include "ColorLutBuilder.hpp"
|
||||
#include "interact/PickInteractorStyle.hpp"
|
||||
|
||||
namespace geopro::render::interact {
|
||||
|
|
@ -48,6 +56,15 @@ void InteractionManager::installStyle() {
|
|||
return true;
|
||||
};
|
||||
interactor_->SetInteractorStyle(style_);
|
||||
|
||||
// 右键菜单观察者:高优先级(1.0)直接挂交互器,先于 vtkImagePlaneWidget(默认 0.0)消费右键。
|
||||
// 命中切片 → handleRightButton 内 abort + 弹菜单;未命中 → 不 abort,事件继续走默认。
|
||||
rightBtnCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
|
||||
rightBtnCmd_->SetClientData(this);
|
||||
rightBtnCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
|
||||
static_cast<InteractionManager*>(client)->handleRightButton();
|
||||
});
|
||||
rightBtnTag_ = interactor_->AddObserver(vtkCommand::RightButtonPressEvent, rightBtnCmd_, 1.0);
|
||||
}
|
||||
|
||||
void InteractionManager::uninstallStyle() {
|
||||
|
|
@ -58,6 +75,12 @@ void InteractionManager::uninstallStyle() {
|
|||
style_->onWheelStep = nullptr;
|
||||
style_->getRotateCenter = nullptr;
|
||||
}
|
||||
// 摘除右键观察者(this 即将析构)。
|
||||
if (interactor_ && rightBtnTag_ != 0) {
|
||||
interactor_->RemoveObserver(rightBtnTag_);
|
||||
rightBtnTag_ = 0;
|
||||
}
|
||||
rightBtnCmd_ = nullptr;
|
||||
// 从 interactor 上彻底摘除自定义 style,避免 interactor 仍持空回调 style(评审 H2)。
|
||||
if (interactor_) interactor_->SetInteractorStyle(nullptr);
|
||||
style_ = nullptr;
|
||||
|
|
@ -145,6 +168,80 @@ void InteractionManager::flipView() {
|
|||
safeRender();
|
||||
}
|
||||
|
||||
void InteractionManager::faceSelected() { faceSlice(selected_); }
|
||||
|
||||
bool InteractionManager::selectedSliceSpec(Vec3& center, Vec3& normal) 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();
|
||||
return true;
|
||||
}
|
||||
|
||||
vtkImageData* InteractionManager::selectedSliceImage() const {
|
||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
|
||||
return slices_[static_cast<std::size_t>(selected_)]->reslicedOutput();
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() const {
|
||||
vtkImageData* scalar = selectedSliceImage();
|
||||
if (scalar == nullptr) return nullptr;
|
||||
|
||||
// 高清导出:切片重采样像素维度受体素网格分辨率限制(常仅几十px)→ 先上采样到目标分辨率
|
||||
// (最长边 kExportLongSide,保持长宽比、插值),再上色,得到清晰大图。
|
||||
constexpr int kExportLongSide = 2048;
|
||||
int dims[3];
|
||||
scalar->GetDimensions(dims);
|
||||
const int nx = dims[0], ny = dims[1];
|
||||
const int longest = std::max(nx, ny);
|
||||
double f = (longest > 0) ? static_cast<double>(kExportLongSide) / longest : 1.0;
|
||||
if (f < 1.0) f = 1.0; // 不缩小(已够大则原样)
|
||||
vtkNew<vtkImageResize> resize;
|
||||
resize->SetInputData(scalar);
|
||||
resize->SetResizeMethodToOutputDimensions();
|
||||
resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)),
|
||||
std::max(1, static_cast<int>(ny * f)), 1);
|
||||
resize->Update();
|
||||
|
||||
// 用与切片显示同一色阶 LUT 上色(colorScale_/vmin_/vmax_ 即当前体/切片着色区间)。
|
||||
auto lut = buildLut(colorScale_, vmin_, vmax_);
|
||||
vtkNew<vtkImageMapToColors> map;
|
||||
map->SetInputConnection(resize->GetOutputPort());
|
||||
map->SetLookupTable(lut);
|
||||
map->SetOutputFormatToRGB();
|
||||
map->Update();
|
||||
auto out = vtkSmartPointer<vtkImageData>::New();
|
||||
out->DeepCopy(map->GetOutput()); // 深拷贝脱离 filter 生命周期
|
||||
return out;
|
||||
}
|
||||
|
||||
void InteractionManager::handleRightButton() {
|
||||
// 高优先级右键观察者(先于 vtkImagePlaneWidget 消费右键)。
|
||||
// 选中目标 = 拾取命中的切片;拾取没命中切片平面(实测常因拾到体/其它面而落在阈值外)则
|
||||
// 回退到"当前选中切片"(左键交互/新建已选中)。有可操作切片 → abort 右键 + 弹菜单;否则放行。
|
||||
if (!interactor_) return;
|
||||
|
||||
int idx = -1;
|
||||
const int* pos = interactor_->GetEventPosition();
|
||||
auto* ren = interactor_->FindPokedRenderer(pos[0], pos[1]);
|
||||
if (ren) {
|
||||
vtkNew<vtkCellPicker> picker;
|
||||
picker->SetTolerance(0.005);
|
||||
if (picker->Pick(pos[0], pos[1], 0.0, ren)) {
|
||||
double w[3];
|
||||
picker->GetPickPosition(w);
|
||||
idx = nearestSlice({w[0], w[1], w[2]});
|
||||
}
|
||||
}
|
||||
if (idx < 0) idx = selected_; // 回退到当前选中切片
|
||||
if (idx < 0 || idx >= static_cast<int>(slices_.size())) return; // 无切片可操作 → 放行默认右键
|
||||
selected_ = idx;
|
||||
updateSelectionVisual();
|
||||
safeRender();
|
||||
if (rightBtnCmd_) rightBtnCmd_->SetAbortFlag(1); // 消费右键,阻止 widget/style 默认行为
|
||||
if (onSliceContextMenuRequested) onSliceContextMenuRequested();
|
||||
}
|
||||
|
||||
int InteractionManager::nearestSlice(const Vec3& worldPoint) const {
|
||||
if (slices_.empty()) return -1;
|
||||
std::vector<Vec3> centers, normals;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#pragma once
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ class vtkImageData;
|
|||
class vtkRenderWindow;
|
||||
class vtkRenderWindowInteractor;
|
||||
class vtkRenderer;
|
||||
class vtkCallbackCommand;
|
||||
|
||||
namespace geopro::render::interact {
|
||||
|
||||
|
|
@ -54,6 +56,18 @@ public:
|
|||
|
||||
// 视图翻转:水平旋转 180°(E55)。
|
||||
void flipView();
|
||||
// 正视当前选中切片(菜单「正视图」入口;无选中则忽略)。
|
||||
void faceSelected();
|
||||
|
||||
// 选中切片位姿(保存切片用):有选中→填 center/normal(世界系) 返回 true;否则 false。
|
||||
bool selectedSliceSpec(Vec3& center, Vec3& normal) const;
|
||||
// 选中切片的重采样 2D 标量影像(导出 dat 用);无选中返回 nullptr。
|
||||
vtkImageData* selectedSliceImage() const;
|
||||
// 选中切片"上色后"的 2D 影像(导出图片用):重采样标量经切片色阶 LUT → RGB 图;无选中返回 nullptr。
|
||||
vtkSmartPointer<vtkImageData> selectedSliceColorImage() const;
|
||||
|
||||
// 右键命中切片时回调(manager 已选中所在切片)→ 上层据此弹切片右键菜单(用 QCursor::pos 定位)。
|
||||
std::function<void()> onSliceContextMenuRequested;
|
||||
|
||||
// 安装/卸载自定义交互样式(构造时安装;析构卸载恢复原样式)。
|
||||
void installStyle();
|
||||
|
|
@ -65,6 +79,10 @@ private:
|
|||
void onDoubleClicked(const Vec3& worldPoint); // 正视所在切片
|
||||
bool onWheel(int dir); // 推进选中切片;无选中返回 false
|
||||
|
||||
// 右键命中切片 → 选中 + 请求弹菜单 + abort(高优先级交互器观察者,先于 vtkImagePlaneWidget
|
||||
// 消费右键,否则 widget 抢走事件、InteractorStyle 永不触发)。未命中切片则不 abort、放行默认。
|
||||
void handleRightButton();
|
||||
|
||||
// 找离世界点最近的切片索引;无切片返回 -1。
|
||||
int nearestSlice(const Vec3& worldPoint) const;
|
||||
// 按 SliceTool 指针设为选中(widget 交互回调用:触碰即选中)。
|
||||
|
|
@ -90,6 +108,9 @@ private:
|
|||
int selected_ = -1; // 选中切片索引(-1=无)
|
||||
|
||||
vtkSmartPointer<PickInteractorStyle> style_;
|
||||
// 右键菜单:高优先级交互器观察者(先于 widget 抢右键)。tag 供 uninstall 时摘除。
|
||||
vtkSmartPointer<vtkCallbackCommand> rightBtnCmd_;
|
||||
unsigned long rightBtnTag_ = 0;
|
||||
|
||||
// 析构进行中:closeAll() 跳过 renderWindow_->Render()(Qt 拆台时窗口可能已半析构,
|
||||
// 析构期再 Render 易崩,评审 M3)。
|
||||
|
|
|
|||
|
|
@ -141,6 +141,10 @@ void SliceTool::advance(double step) {
|
|||
widget_->UpdatePlacement();
|
||||
}
|
||||
|
||||
vtkImageData* SliceTool::reslicedOutput() const {
|
||||
return widget_ ? widget_->GetResliceOutput() : nullptr;
|
||||
}
|
||||
|
||||
double SliceTool::distanceToPlane(const Vec3& p) const {
|
||||
const Vec3 c = center();
|
||||
const Vec3 n = normal();
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ public:
|
|||
// 世界点到本切面(无限平面)的垂直距离绝对值。供 picker 命中判定"点在哪张切片上"。
|
||||
double distanceToPlane(const Vec3& worldPoint) const;
|
||||
|
||||
// 当前切面重采样得到的 2D 标量影像(导出 dat 用);widget 已释放则 nullptr。
|
||||
vtkImageData* reslicedOutput() const;
|
||||
|
||||
// 关闭:Off() 并解除 interactor 绑定(幂等)。
|
||||
void close();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue