feat(vtk): P3 三维分析切片交互(核心) — 轴向/任意切片+滚轮+拾取+正视
interact/ 交互层(README早规划,本期落地): - SlicePlaneMath: 纯几何(法向/45°/滚轮平移/双击正视相机含竖直兜底/最近切片), 25 单测 - SliceTool: 封装 vtkImagePlaneWidget — 轴向(上下/前后/左右,角度固定)+任意45°(Origin/Pt1/Pt2), 套色阶LUT, reslice着色(非cutter), close幂等 - PickInteractorStyle: 继承TrackballCamera+vtkCellPicker, 拾取/双击/滚轮回调 - InteractionManager: 活动切片/选中态/滚轮分发/拾取联动/翻转, 体素变更先closeAll再附着 - VtkSceneView 暴露 currentVolumeImage_(含VE) + onVolumeChanged; main.cpp 切片工具条(上下/前后/左右/任意/翻转/关闭) - ctest 221/221 评审修复: - H1 vtkTrivialProducer 提为成员(局部变量构造后析构→管线断裂崩溃) - H2 uninstallStyle 向 interactor 注销 style - H3 safeRender 统一守 destroying_ 跳过析构期 Render - M1 advance 刚性平移 origin+point1+point2(只移origin致轴向切面变形) - M2 closeSelected 选中位就近不跳0; M4 sliceBar 加 BottomLeftAnchor 随resize 范围外(P4): 切片保存/导出/删除为数据集/三维分析树/右键菜单/异常圈定/详情
This commit is contained in:
parent
c44203d6ca
commit
85d4ff57df
|
|
@ -62,6 +62,9 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render
|
||||||
void VtkSceneView::clear() {
|
void VtkSceneView::clear() {
|
||||||
scene_.clear(); // RemoveAllViewProps:连同坐标轴一并移除
|
scene_.clear(); // RemoveAllViewProps:连同坐标轴一并移除
|
||||||
currentAxes_ = nullptr; // 旧坐标轴已随 clear 移除,置空避免悬留引用
|
currentAxes_ = nullptr; // 旧坐标轴已随 clear 移除,置空避免悬留引用
|
||||||
|
// 体素 image 失效:置空并通知上层关闭切片(防切片附着到已移除的 image)。
|
||||||
|
currentVolumeImage_ = nullptr;
|
||||||
|
if (onVolumeChanged) onVolumeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
|
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
|
||||||
|
|
@ -81,10 +84,20 @@ void VtkSceneView::addCurtain(const geopro::core::Grid& grid, const geopro::core
|
||||||
|
|
||||||
void VtkSceneView::addVolume(const geopro::data::VolumeGrid& vol, const geopro::core::ColorScale& cs) {
|
void VtkSceneView::addVolume(const geopro::data::VolumeGrid& vol, const geopro::core::ColorScale& cs) {
|
||||||
// 纵向夸张烤进 image 的 z 原点/间距(与帘面 SetScale 同倍,保证纵向一致)。
|
// 纵向夸张烤进 image 的 z 原点/间距(与帘面 SetScale 同倍,保证纵向一致)。
|
||||||
|
// 用暴露 image 的 buildVoxel 重载:保留 currentVolumeImage_ 供 P3 切片附着(几何含 VE)。
|
||||||
|
vtkSmartPointer<vtkImageData> image;
|
||||||
auto volume = geopro::render::buildVoxel(
|
auto volume = geopro::render::buildVoxel(
|
||||||
vol.vol, cs, vol.origin[0], vol.origin[1], vol.origin[2] * verticalExaggeration_,
|
vol.vol, cs, vol.origin[0], vol.origin[1], vol.origin[2] * verticalExaggeration_,
|
||||||
vol.spacing[0], vol.spacing[1], vol.spacing[2] * verticalExaggeration_, vol.vmin, vol.vmax);
|
vol.spacing[0], vol.spacing[1], vol.spacing[2] * verticalExaggeration_, vol.vmin, vol.vmax,
|
||||||
if (volume) scene_.addViewProp(volume);
|
image);
|
||||||
|
if (volume) {
|
||||||
|
scene_.addViewProp(volume);
|
||||||
|
currentVolumeImage_ = image;
|
||||||
|
currentColorScale_ = cs;
|
||||||
|
currentVmin_ = vol.vmin;
|
||||||
|
currentVmax_ = vol.vmax;
|
||||||
|
if (onVolumeChanged) onVolumeChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
|
void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <vtkCubeAxesActor.h>
|
#include <vtkCubeAxesActor.h>
|
||||||
|
#include <vtkImageData.h>
|
||||||
#include <vtkSmartPointer.h>
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
#include "I3dSceneView.hpp"
|
#include "I3dSceneView.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
|
||||||
namespace geopro::core { class GeoLocalFrame; }
|
namespace geopro::core { class GeoLocalFrame; }
|
||||||
namespace geopro::render { class Scene; }
|
namespace geopro::render { class Scene; }
|
||||||
|
|
@ -36,6 +39,18 @@ public:
|
||||||
void fitView() override;
|
void fitView() override;
|
||||||
void render(bool is2D) override;
|
void render(bool is2D) override;
|
||||||
|
|
||||||
|
// ── P3 切片交互:暴露当前体素 image(含 VE 烤入的 origin/spacing)供切片附着 ──
|
||||||
|
// addVolume 用暴露 image 的 buildVoxel 重载保留;clear/无体素时置空。
|
||||||
|
vtkImageData* currentVolumeImage() const { return currentVolumeImage_.Get(); }
|
||||||
|
const geopro::core::ColorScale& currentColorScale() const { return currentColorScale_; }
|
||||||
|
double currentVmin() const { return currentVmin_; }
|
||||||
|
double currentVmax() const { return currentVmax_; }
|
||||||
|
bool hasVolume() const { return currentVolumeImage_ != nullptr; }
|
||||||
|
|
||||||
|
// 体素 image 变化(addVolume 附着新 image / clear 置空)时回调,供上层把新 image 推给
|
||||||
|
// InteractionManager(重附着或关闭切片)。clear 时以 nullptr 触发。
|
||||||
|
std::function<void()> onVolumeChanged;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// 按当前坐标轴设置 + 场景包围盒重建坐标轴 prop(render 末尾调)。
|
// 按当前坐标轴设置 + 场景包围盒重建坐标轴 prop(render 末尾调)。
|
||||||
void rebuildAxes();
|
void rebuildAxes();
|
||||||
|
|
@ -53,6 +68,12 @@ private:
|
||||||
// 当前坐标轴 prop:render 可能多次调用 rebuildAxes(rebuild 末尾 + 异步回灌),
|
// 当前坐标轴 prop:render 可能多次调用 rebuildAxes(rebuild 末尾 + 异步回灌),
|
||||||
// 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。
|
// 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。
|
||||||
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
|
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
|
||||||
|
|
||||||
|
// 当前体素 image + 色阶(P3 切片附着源);无体素时为空。
|
||||||
|
vtkSmartPointer<vtkImageData> currentVolumeImage_;
|
||||||
|
geopro::core::ColorScale currentColorScale_;
|
||||||
|
double currentVmin_ = 0.0;
|
||||||
|
double currentVmax_ = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
137
src/app/main.cpp
137
src/app/main.cpp
|
|
@ -121,6 +121,8 @@
|
||||||
#include "ColorLutBuilder.hpp"
|
#include "ColorLutBuilder.hpp"
|
||||||
#include "Scene.hpp"
|
#include "Scene.hpp"
|
||||||
#include "VoxelFromScatters.hpp"
|
#include "VoxelFromScatters.hpp"
|
||||||
|
#include "interact/InteractionManager.hpp"
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
#include "actors/AnomalyActor.hpp"
|
#include "actors/AnomalyActor.hpp"
|
||||||
#include "actors/CurtainActor.hpp"
|
#include "actors/CurtainActor.hpp"
|
||||||
#include "actors/ElectrodeActor.hpp"
|
#include "actors/ElectrodeActor.hpp"
|
||||||
|
|
@ -212,6 +214,31 @@ private:
|
||||||
QWidget* header_;
|
QWidget* header_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 浮层左下角锚定:随 host 尺寸变化贴左下(切片工具条用,评审 M4)。
|
||||||
|
class BottomLeftAnchor : public QObject {
|
||||||
|
public:
|
||||||
|
BottomLeftAnchor(QWidget* overlay, QWidget* host) : QObject(host), overlay_(overlay), host_(host)
|
||||||
|
{
|
||||||
|
host_->installEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* obj, QEvent* e) override
|
||||||
|
{
|
||||||
|
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show) &&
|
||||||
|
overlay_->isVisible()) {
|
||||||
|
overlay_->adjustSize();
|
||||||
|
overlay_->move(14, host_->height() - overlay_->height() - 14);
|
||||||
|
overlay_->raise();
|
||||||
|
}
|
||||||
|
return QObject::eventFilter(obj, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWidget* overlay_;
|
||||||
|
QWidget* host_;
|
||||||
|
};
|
||||||
|
|
||||||
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
||||||
std::string readPem(const std::string& path)
|
std::string readPem(const std::string& path)
|
||||||
{
|
{
|
||||||
|
|
@ -275,9 +302,19 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
||||||
vtkWidget);
|
vtkWidget);
|
||||||
sceneCtrl->setVerticalExaggeration(kVerticalExaggeration);
|
sceneCtrl->setVerticalExaggeration(kVerticalExaggeration);
|
||||||
// 非 QObject 堆对象统一在此清理,按构造逆序:sceneView(持 scene&) → scene3dRepo → scene。
|
|
||||||
// (sceneCtrl 是 vtkWidget 的 QObject 子对象,由 Qt 在 destroyed 前先析构,不再触发信号回灌。)
|
// ── P3 切片交互编排(InteractionManager)─────────────────────────────────
|
||||||
QObject::connect(vtkWidget, &QObject::destroyed, [scene, scene3dRepo, sceneView]() {
|
// interactor 由 QVTK 在 setRenderWindow 后提供(renderWindow->GetInteractor())。
|
||||||
|
// 安装自定义拾取样式 + 持活动切片。仅三维 + 有体素可用;切到二维 closeAll。
|
||||||
|
auto* interactionMgr = new geopro::render::interact::InteractionManager(
|
||||||
|
renderWindowPtr->GetInteractor(), renderWindowPtr, scene->renderer());
|
||||||
|
// sceneView->onVolumeChanged 在切片 UI 接线处统一设置(需 updateSliceButtons 闭包,见下)。
|
||||||
|
// 非 QObject 堆对象统一在此清理,按构造逆序:
|
||||||
|
// interactionMgr(持 interactor/切片观察者) → sceneView(持 scene&) → scene3dRepo → scene。
|
||||||
|
// interactionMgr 先析构:closeAll() 解绑所有切片观察者,再拆 scene/interactor,防悬挂崩溃。
|
||||||
|
QObject::connect(vtkWidget, &QObject::destroyed,
|
||||||
|
[scene, scene3dRepo, sceneView, interactionMgr]() {
|
||||||
|
delete interactionMgr;
|
||||||
delete sceneView;
|
delete sceneView;
|
||||||
delete scene3dRepo;
|
delete scene3dRepo;
|
||||||
delete scene;
|
delete scene;
|
||||||
|
|
@ -487,6 +524,86 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 锚定器 parent=centerWidget,随其销毁;不需保留指针。
|
// 锚定器 parent=centerWidget,随其销毁;不需保留指针。
|
||||||
new RightTopAnchor(axisBar, centerWidget, viewHeader);
|
new RightTopAnchor(axisBar, centerWidget, viewHeader);
|
||||||
|
|
||||||
|
// ──「切片」工具条浮层(P3,spec §9):浮于 QVTK 左下,仅三维 + 有体素时可用。
|
||||||
|
// 上下/前后/左右/任意 → 创建对应切片;关闭 → 关当前选中切片;翻转 → 水平 180°。
|
||||||
|
// 深色主题复用 P2 工具条同款样式(canvas/* token,不设 border-radius,GL 上四角露浅底)。
|
||||||
|
auto* sliceBar = new QFrame(centerWidget);
|
||||||
|
sliceBar->setFrameShape(QFrame::StyledPanel);
|
||||||
|
geopro::app::applyTokenizedStyleSheet(
|
||||||
|
sliceBar,
|
||||||
|
QStringLiteral(
|
||||||
|
"QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};}"
|
||||||
|
"QLabel{color:{{canvas/text}};border:none;background:transparent;}"
|
||||||
|
"QPushButton{color:{{canvas/text}};background:{{canvas/bg}};border:1px solid {{canvas/grid}};"
|
||||||
|
"border-radius:4px;padding:2px 8px;}"
|
||||||
|
"QPushButton:hover{background:{{bg/hover}};border-color:{{accent/primary}};}"
|
||||||
|
"QPushButton:pressed{background:{{bg/selected}};}"
|
||||||
|
"QPushButton:disabled{color:{{canvas/text-dim}};border-color:{{canvas/grid}};}"));
|
||||||
|
auto* sliceLayout = new QHBoxLayout(sliceBar);
|
||||||
|
sliceLayout->setContentsMargins(geopro::app::space::kMd, geopro::app::space::kSm,
|
||||||
|
geopro::app::space::kMd, geopro::app::space::kSm);
|
||||||
|
sliceLayout->setSpacing(geopro::app::space::kSm);
|
||||||
|
auto* sliceLabel = new QLabel(QStringLiteral("切片"));
|
||||||
|
auto* btnSliceUpDown = new QPushButton(QStringLiteral("上下"));
|
||||||
|
auto* btnSliceFrontBack = new QPushButton(QStringLiteral("前后"));
|
||||||
|
auto* btnSliceLeftRight = new QPushButton(QStringLiteral("左右"));
|
||||||
|
auto* btnSliceOblique = new QPushButton(QStringLiteral("任意"));
|
||||||
|
auto* btnSliceFlip = new QPushButton(QStringLiteral("翻转"));
|
||||||
|
auto* btnSliceClose = new QPushButton(QStringLiteral("关闭"));
|
||||||
|
sliceLayout->addWidget(sliceLabel);
|
||||||
|
sliceLayout->addWidget(btnSliceUpDown);
|
||||||
|
sliceLayout->addWidget(btnSliceFrontBack);
|
||||||
|
sliceLayout->addWidget(btnSliceLeftRight);
|
||||||
|
sliceLayout->addWidget(btnSliceOblique);
|
||||||
|
sliceLayout->addWidget(btnSliceFlip);
|
||||||
|
sliceLayout->addWidget(btnSliceClose);
|
||||||
|
sliceBar->setVisible(false); // 默认二维,不显示
|
||||||
|
new BottomLeftAnchor(sliceBar, centerWidget); // 随窗口 resize 贴左下(评审 M4)
|
||||||
|
|
||||||
|
// 切片按钮可用性:仅三维 + 有体素时创建/翻转可用;关闭仅在有切片时可用。
|
||||||
|
auto updateSliceButtons = [interactionMgr, btnSliceUpDown, btnSliceFrontBack, btnSliceLeftRight,
|
||||||
|
btnSliceOblique, btnSliceFlip, btnSliceClose, sceneView]() {
|
||||||
|
const bool canSlice = sceneView->hasVolume() && interactionMgr->hasVolume();
|
||||||
|
btnSliceUpDown->setEnabled(canSlice);
|
||||||
|
btnSliceFrontBack->setEnabled(canSlice);
|
||||||
|
btnSliceLeftRight->setEnabled(canSlice);
|
||||||
|
btnSliceOblique->setEnabled(canSlice);
|
||||||
|
btnSliceFlip->setEnabled(canSlice);
|
||||||
|
btnSliceClose->setEnabled(interactionMgr->hasSlices());
|
||||||
|
};
|
||||||
|
updateSliceButtons();
|
||||||
|
|
||||||
|
using SliceAxis = geopro::render::interact::SliceAxis;
|
||||||
|
auto addSlice = [interactionMgr, updateSliceButtons](SliceAxis axis) {
|
||||||
|
interactionMgr->addSlice(axis);
|
||||||
|
updateSliceButtons();
|
||||||
|
};
|
||||||
|
QObject::connect(btnSliceUpDown, &QPushButton::clicked, vtkWidget,
|
||||||
|
[addSlice]() { addSlice(SliceAxis::UpDown); });
|
||||||
|
QObject::connect(btnSliceFrontBack, &QPushButton::clicked, vtkWidget,
|
||||||
|
[addSlice]() { addSlice(SliceAxis::FrontBack); });
|
||||||
|
QObject::connect(btnSliceLeftRight, &QPushButton::clicked, vtkWidget,
|
||||||
|
[addSlice]() { addSlice(SliceAxis::LeftRight); });
|
||||||
|
QObject::connect(btnSliceOblique, &QPushButton::clicked, vtkWidget,
|
||||||
|
[addSlice]() { addSlice(SliceAxis::Oblique); });
|
||||||
|
QObject::connect(btnSliceFlip, &QPushButton::clicked, vtkWidget,
|
||||||
|
[interactionMgr]() { interactionMgr->flipView(); });
|
||||||
|
QObject::connect(btnSliceClose, &QPushButton::clicked, vtkWidget,
|
||||||
|
[interactionMgr, updateSliceButtons]() {
|
||||||
|
interactionMgr->closeSelected();
|
||||||
|
updateSliceButtons();
|
||||||
|
});
|
||||||
|
// 体素变化(重建/清场)后刷新按钮可用性(切片可能已被 closeAll 清空)。
|
||||||
|
sceneView->onVolumeChanged = [interactionMgr, sceneView, updateSliceButtons]() {
|
||||||
|
if (sceneView->hasVolume())
|
||||||
|
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
|
||||||
|
sceneView->currentColorScale(), sceneView->currentVmin(),
|
||||||
|
sceneView->currentVmax());
|
||||||
|
else
|
||||||
|
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
|
||||||
|
updateSliceButtons();
|
||||||
|
};
|
||||||
|
|
||||||
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
|
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
|
||||||
// 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中;
|
// 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中;
|
||||||
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
|
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
|
||||||
|
|
@ -689,7 +806,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
|
|
||||||
// 「视图详情」浮层 + 「三维数据集栏」工具条显隐:仅三维显示。
|
// 「视图详情」浮层 + 「三维数据集栏」工具条显隐:仅三维显示。
|
||||||
// 视图详情浮层置左上;P2 工具条置右上(工具条下方),二者均随相机/数据变化保持位置。
|
// 视图详情浮层置左上;P2 工具条置右上(工具条下方),二者均随相机/数据变化保持位置。
|
||||||
auto showLayerPanel = [layerPanel, axisBar, viewHeader, centerWidget](bool show3D) {
|
auto showLayerPanel = [layerPanel, axisBar, sliceBar, viewHeader, centerWidget,
|
||||||
|
updateSliceButtons](bool show3D) {
|
||||||
if (show3D) {
|
if (show3D) {
|
||||||
layerPanel->move(14, viewHeader->height() + 12);
|
layerPanel->move(14, viewHeader->height() + 12);
|
||||||
layerPanel->adjustSize();
|
layerPanel->adjustSize();
|
||||||
|
|
@ -700,9 +818,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
axisBar->move(centerWidget->width() - axisBar->width() - 14, viewHeader->height() + 12);
|
axisBar->move(centerWidget->width() - axisBar->width() - 14, viewHeader->height() + 12);
|
||||||
axisBar->setVisible(true);
|
axisBar->setVisible(true);
|
||||||
axisBar->raise();
|
axisBar->raise();
|
||||||
|
// 切片工具条:左下角(视图详情浮层下方)。
|
||||||
|
sliceBar->adjustSize();
|
||||||
|
sliceBar->move(14, centerWidget->height() - sliceBar->height() - 14);
|
||||||
|
sliceBar->setVisible(true);
|
||||||
|
sliceBar->raise();
|
||||||
|
updateSliceButtons();
|
||||||
} else {
|
} else {
|
||||||
layerPanel->setVisible(false);
|
layerPanel->setVisible(false);
|
||||||
axisBar->setVisible(false);
|
axisBar->setVisible(false);
|
||||||
|
sliceBar->setVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -710,7 +835,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
using geopro::controller::SceneLayer;
|
using geopro::controller::SceneLayer;
|
||||||
using CtrlViewMode = geopro::controller::ViewMode;
|
using CtrlViewMode = geopro::controller::ViewMode;
|
||||||
QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget,
|
QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget,
|
||||||
[sceneCtrl, showLayerPanel]() {
|
[sceneCtrl, showLayerPanel, interactionMgr, updateSliceButtons]() {
|
||||||
|
interactionMgr->closeAll(); // 切到二维:关闭所有切片(仅三维有切片)
|
||||||
|
updateSliceButtons();
|
||||||
showLayerPanel(false);
|
showLayerPanel(false);
|
||||||
sceneCtrl->setViewMode(CtrlViewMode::Map2D);
|
sceneCtrl->setViewMode(CtrlViewMode::Map2D);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 RenderingAnnotation InteractionStyle InteractionWidgets IOImage)
|
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 RenderingAnnotation InteractionStyle InteractionWidgets ImagingCore IOImage)
|
||||||
find_package(GDAL CONFIG REQUIRED)
|
find_package(GDAL CONFIG REQUIRED)
|
||||||
add_library(geopro_render STATIC
|
add_library(geopro_render STATIC
|
||||||
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp)
|
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp
|
||||||
|
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp)
|
||||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
||||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
- `actors/` — ScatterActor, GridContourActor, VoxelVolumeActor, AnomalyActor, TerrainActor
|
- `actors/` — ScatterActor, GridContourActor, VoxelVolumeActor, AnomalyActor, TerrainActor
|
||||||
- `color/` — ColorLutBuilder(colorBar → 离散 vtkLookupTable), ScalarBar
|
- `color/` — ColorLutBuilder(colorBar → 离散 vtkLookupTable), ScalarBar
|
||||||
- `camera/` — CameraPreset(Top2D / Free3D)
|
- `camera/` — CameraPreset(Top2D / Free3D)
|
||||||
- `interact/` — InteractionManager + InteractionTool(Measure/Slice/PickSelect);切片用 vtkResliceCursorWidget
|
- `interact/` — SlicePlaneMath(纯几何,可测)+ SliceTool(vtkImagePlaneWidget:轴向 + 任意 45° reslice 着色剖面)+ PickInteractorStyle(拾取/双击正视/滚轮)+ InteractionManager(持切片/选中态/分发)。切片走 vtkImageReslice 路线(vtkImagePlaneWidget 内部 reslice + 纹理),非 vtkCutter(spec §9.1)
|
||||||
- `ground/` — IGroundLayer + DemImageGroundLayer(M1);TileGroundLayer(M1.5)
|
- `ground/` — IGroundLayer + DemImageGroundLayer(M1);TileGroundLayer(M1.5)
|
||||||
|
|
||||||
网格管线:`vtkImageData(+vtkWarpScalar) → vtkDataSetSurfaceFilter → vtkBandedPolyDataContourFilter(GenerateContourEdgesOn)`(设计 §4.3)。
|
网格管线:`vtkImageData(+vtkWarpScalar) → vtkDataSetSurfaceFilter → vtkBandedPolyDataContourFilter(GenerateContourEdgesOn)`(设计 §4.3)。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
#include "interact/InteractionManager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <vtkCamera.h>
|
||||||
|
#include <vtkImageData.h>
|
||||||
|
#include <vtkRenderWindow.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
|
#include <vtkRenderer.h>
|
||||||
|
|
||||||
|
#include "interact/PickInteractorStyle.hpp"
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::array<double, 6> imageBounds(vtkImageData* img) {
|
||||||
|
std::array<double, 6> b{{0, 0, 0, 0, 0, 0}};
|
||||||
|
if (img) img->GetBounds(b.data());
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
InteractionManager::InteractionManager(vtkRenderWindowInteractor* interactor,
|
||||||
|
vtkRenderWindow* renderWindow, vtkRenderer* renderer)
|
||||||
|
: interactor_(interactor), renderWindow_(renderWindow), renderer_(renderer) {
|
||||||
|
installStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractionManager::~InteractionManager() {
|
||||||
|
destroying_ = true; // closeAll 跳过 Render(Qt 拆台时窗口可能已半析构)
|
||||||
|
closeAll();
|
||||||
|
uninstallStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::installStyle() {
|
||||||
|
if (!interactor_ || style_) return;
|
||||||
|
style_ = vtkSmartPointer<PickInteractorStyle>::New();
|
||||||
|
style_->onPick = [this](const Vec3& w) { onPicked(w); };
|
||||||
|
style_->onDoubleClick = [this](const Vec3& w) { onDoubleClicked(w); };
|
||||||
|
style_->onWheelStep = [this](int dir) { return onWheel(dir); };
|
||||||
|
interactor_->SetInteractorStyle(style_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::uninstallStyle() {
|
||||||
|
if (style_) {
|
||||||
|
// 断开回调(this 即将析构),避免迟到事件回调悬垂。
|
||||||
|
style_->onPick = nullptr;
|
||||||
|
style_->onDoubleClick = nullptr;
|
||||||
|
style_->onWheelStep = nullptr;
|
||||||
|
}
|
||||||
|
// 从 interactor 上彻底摘除自定义 style,避免 interactor 仍持空回调 style(评审 H2)。
|
||||||
|
if (interactor_) interactor_->SetInteractorStyle(nullptr);
|
||||||
|
style_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::safeRender() {
|
||||||
|
if (renderWindow_ && !destroying_) renderWindow_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::addSlice(SliceAxis axis) {
|
||||||
|
if (!image_ || !interactor_) return;
|
||||||
|
auto tool = std::make_unique<SliceTool>(image_, interactor_, axis, colorScale_, vmin_, vmax_);
|
||||||
|
slices_.push_back(std::move(tool));
|
||||||
|
selected_ = static_cast<int>(slices_.size()) - 1; // 新切片选中
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::closeSelected() {
|
||||||
|
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return;
|
||||||
|
slices_[static_cast<std::size_t>(selected_)]->close();
|
||||||
|
slices_.erase(slices_.begin() + selected_);
|
||||||
|
// 选中停在原位就近(删后该位变成下一张;删的是末张则退一张),不跳回 0(评审 M2)。
|
||||||
|
selected_ = slices_.empty() ? -1
|
||||||
|
: std::min(selected_, static_cast<int>(slices_.size()) - 1);
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::closeAll() {
|
||||||
|
for (auto& s : slices_) s->close(); // 显式 Off + 解绑(析构亦会,双保险幂等)
|
||||||
|
slices_.clear();
|
||||||
|
selected_ = -1;
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::flipView() {
|
||||||
|
if (!renderer_) return;
|
||||||
|
auto* cam = renderer_->GetActiveCamera();
|
||||||
|
if (!cam) return;
|
||||||
|
cam->Azimuth(180.0); // 水平旋转 180°(E55)
|
||||||
|
cam->OrthogonalizeViewUp();
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
int InteractionManager::nearestSlice(const Vec3& worldPoint) const {
|
||||||
|
if (slices_.empty()) return -1;
|
||||||
|
std::vector<Vec3> centers, normals;
|
||||||
|
centers.reserve(slices_.size());
|
||||||
|
normals.reserve(slices_.size());
|
||||||
|
for (const auto& s : slices_) {
|
||||||
|
centers.push_back(s->center());
|
||||||
|
normals.push_back(s->normal());
|
||||||
|
}
|
||||||
|
const int idx = nearestPlane(centers, normals, worldPoint);
|
||||||
|
if (idx < 0) return -1;
|
||||||
|
// 阈值:命中点离最近切面太远(> 体对角线 5%)视为"没点在切片上",不改选中(评审 M2)。
|
||||||
|
const std::array<double, 6> b = imageBounds(image_);
|
||||||
|
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);
|
||||||
|
if (diag > 0.0 && dist > diag * 0.05) return -1;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::onPicked(const Vec3& worldPoint) {
|
||||||
|
// 焦点设到命中点 → 拖动绕其旋转(spec C38/D39)。
|
||||||
|
if (renderer_) {
|
||||||
|
if (auto* cam = renderer_->GetActiveCamera())
|
||||||
|
cam->SetFocalPoint(worldPoint[0], worldPoint[1], worldPoint[2]);
|
||||||
|
}
|
||||||
|
// 若命中点落在某切片上(阈值内),选中之(供滚轮推进/关闭)。
|
||||||
|
const int idx = nearestSlice(worldPoint);
|
||||||
|
if (idx >= 0) selected_ = idx;
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractionManager::onDoubleClicked(const Vec3& worldPoint) {
|
||||||
|
const int idx = nearestSlice(worldPoint);
|
||||||
|
if (idx < 0 || !renderer_) return;
|
||||||
|
auto* cam = renderer_->GetActiveCamera();
|
||||||
|
if (!cam) return;
|
||||||
|
selected_ = idx;
|
||||||
|
const Vec3 focal = slices_[static_cast<std::size_t>(idx)]->center();
|
||||||
|
const Vec3 normal = slices_[static_cast<std::size_t>(idx)]->normal();
|
||||||
|
const double dist = cam->GetDistance(); // 保持当前观察距离
|
||||||
|
const FaceOnCamera face = faceOnCamera(focal, normal, dist);
|
||||||
|
cam->SetFocalPoint(focal[0], focal[1], focal[2]);
|
||||||
|
cam->SetPosition(face.position[0], face.position[1], face.position[2]);
|
||||||
|
cam->SetViewUp(face.viewUp[0], face.viewUp[1], face.viewUp[2]);
|
||||||
|
cam->OrthogonalizeViewUp();
|
||||||
|
renderer_->ResetCameraClippingRange();
|
||||||
|
safeRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InteractionManager::onWheel(int dir) {
|
||||||
|
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
|
||||||
|
const double step = wheelStep(imageBounds(image_), dir);
|
||||||
|
slices_[static_cast<std::size_t>(selected_)]->advance(step);
|
||||||
|
safeRender();
|
||||||
|
return true; // 消费滚轮(不缩放)
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
|
#include "interact/SliceTool.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
|
||||||
|
class vtkImageData;
|
||||||
|
class vtkRenderWindow;
|
||||||
|
class vtkRenderWindowInteractor;
|
||||||
|
class vtkRenderer;
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
class PickInteractorStyle;
|
||||||
|
|
||||||
|
// 三维切片交互编排(spec §9):持 interactor + 活动切片列表 + 选中态。
|
||||||
|
// · 创建/关闭切片(轴向/任意),附着到当前体素 image(含 VE 烤入的几何)。
|
||||||
|
// · 安装自定义 PickInteractorStyle:拾取选中→绕命中点旋转;双击切片→正视;滚轮→沿法向推进选中切片。
|
||||||
|
// · 视图翻转(水平 Azimuth 180°,E55)。
|
||||||
|
// · 切到二维 / 体素重建 / 清场:closeAll 安全释放所有切片(Off + 解绑,防悬挂观察者崩溃)。
|
||||||
|
//
|
||||||
|
// render 层:只碰 VTK widget/相机,不认仓储;产物经回调/上层处理(本期切片仅在视图内交互)。
|
||||||
|
class InteractionManager {
|
||||||
|
public:
|
||||||
|
// interactor:QVTK 提供的活 interactor(renderWindow->GetInteractor())。
|
||||||
|
// renderWindow:用于推进/翻转后重绘。
|
||||||
|
InteractionManager(vtkRenderWindowInteractor* interactor, vtkRenderWindow* renderWindow,
|
||||||
|
vtkRenderer* renderer);
|
||||||
|
~InteractionManager();
|
||||||
|
|
||||||
|
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 则忽略。新切片自动设为选中。
|
||||||
|
void addSlice(SliceAxis axis);
|
||||||
|
|
||||||
|
// 关闭选中切片(E56)。无选中则忽略。
|
||||||
|
void closeSelected();
|
||||||
|
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
|
||||||
|
void closeAll();
|
||||||
|
|
||||||
|
bool hasVolume() const { return image_ != nullptr; }
|
||||||
|
bool hasSlices() const { return !slices_.empty(); }
|
||||||
|
int sliceCount() const { return static_cast<int>(slices_.size()); }
|
||||||
|
|
||||||
|
// 视图翻转:水平旋转 180°(E55)。
|
||||||
|
void flipView();
|
||||||
|
|
||||||
|
// 安装/卸载自定义交互样式(构造时安装;析构卸载恢复原样式)。
|
||||||
|
void installStyle();
|
||||||
|
void uninstallStyle();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 拾取回调实现(PickInteractorStyle 注入)。
|
||||||
|
void onPicked(const Vec3& worldPoint); // 选中所在切片 + 焦点
|
||||||
|
void onDoubleClicked(const Vec3& worldPoint); // 正视所在切片
|
||||||
|
bool onWheel(int dir); // 推进选中切片;无选中返回 false
|
||||||
|
|
||||||
|
// 找离世界点最近的切片索引;无切片返回 -1。
|
||||||
|
int nearestSlice(const Vec3& worldPoint) const;
|
||||||
|
|
||||||
|
// 统一重绘:析构进行中(destroying_)跳过,避免 Qt 拆台时对半析构窗口 Render 崩溃(评审 H3)。
|
||||||
|
void safeRender();
|
||||||
|
|
||||||
|
vtkRenderWindowInteractor* interactor_;
|
||||||
|
vtkRenderWindow* renderWindow_;
|
||||||
|
vtkRenderer* renderer_;
|
||||||
|
|
||||||
|
vtkImageData* image_ = nullptr; // 非拥有;当前体素 image
|
||||||
|
geopro::core::ColorScale colorScale_;
|
||||||
|
double vmin_ = 0.0, vmax_ = 0.0;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<SliceTool>> slices_;
|
||||||
|
int selected_ = -1; // 选中切片索引(-1=无)
|
||||||
|
|
||||||
|
vtkSmartPointer<PickInteractorStyle> style_;
|
||||||
|
|
||||||
|
// 析构进行中:closeAll() 跳过 renderWindow_->Render()(Qt 拆台时窗口可能已半析构,
|
||||||
|
// 析构期再 Render 易崩,评审 M3)。
|
||||||
|
bool destroying_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
#include "interact/PickInteractorStyle.hpp"
|
||||||
|
|
||||||
|
#include <vtkCellPicker.h>
|
||||||
|
#include <vtkNew.h>
|
||||||
|
#include <vtkObjectFactory.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
|
#include <vtkRenderer.h>
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
vtkStandardNewMacro(PickInteractorStyle);
|
||||||
|
|
||||||
|
bool PickInteractorStyle::pickWorld(Vec3& out) {
|
||||||
|
auto* iren = this->GetInteractor();
|
||||||
|
if (!iren) return false;
|
||||||
|
const int* pos = iren->GetEventPosition();
|
||||||
|
// 用交互器解析被点中的 renderer(基类 FindPokedRenderer 仅设 CurrentRenderer、返回 void)。
|
||||||
|
auto* ren = iren->FindPokedRenderer(pos[0], pos[1]);
|
||||||
|
if (!ren) return false;
|
||||||
|
// CellPicker:返回表面交点世界坐标(命中切片纹理面/帘面等)。
|
||||||
|
vtkNew<vtkCellPicker> picker;
|
||||||
|
picker->SetTolerance(0.005);
|
||||||
|
if (!picker->Pick(pos[0], pos[1], 0.0, ren)) return false;
|
||||||
|
double w[3];
|
||||||
|
picker->GetPickPosition(w);
|
||||||
|
out = {w[0], w[1], w[2]};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PickInteractorStyle::OnLeftButtonDown() {
|
||||||
|
auto* iren = this->GetInteractor();
|
||||||
|
Vec3 world;
|
||||||
|
const bool hit = pickWorld(world);
|
||||||
|
|
||||||
|
if (hit && iren && iren->GetRepeatCount() > 0) {
|
||||||
|
// 双击命中 → 正视所在切片(交给 manager 找最近切片 + 算相机)。
|
||||||
|
if (onDoubleClick) onDoubleClick(world);
|
||||||
|
return; // 不进入拖动旋转
|
||||||
|
}
|
||||||
|
if (hit) {
|
||||||
|
// 单击命中 → 选中 + 以命中点为焦点(拖动绕其旋转)。
|
||||||
|
if (onPick) onPick(world);
|
||||||
|
}
|
||||||
|
// 始终保留 TrackballCamera 默认拖动(旋转/平移)。
|
||||||
|
Superclass::OnLeftButtonDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PickInteractorStyle::OnMouseWheelForward() {
|
||||||
|
if (onWheelStep && onWheelStep(+1)) return; // 有选中切片 → 推进,消费滚轮
|
||||||
|
Superclass::OnMouseWheelForward(); // 否则默认缩放
|
||||||
|
}
|
||||||
|
|
||||||
|
void PickInteractorStyle::OnMouseWheelBackward() {
|
||||||
|
if (onWheelStep && onWheelStep(-1)) return;
|
||||||
|
Superclass::OnMouseWheelBackward();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include <vtkInteractorStyleTrackballCamera.h>
|
||||||
|
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
// 自定义交互样式:在 TrackballCamera 基础上加拾取与切片交互(spec §9.3)。
|
||||||
|
// 左键按下 → vtkPropPicker 拾取 → 命中则相机 focalPoint=命中点(拖动绕其旋转),
|
||||||
|
// 并把命中世界点回调出去(InteractionManager 据此选中所在切片)。
|
||||||
|
// 左键双击 → 回调双击世界点(InteractionManager 找最近切片 → 相机正视其法向)。
|
||||||
|
// 滚轮前/后 → 回调步进方向(±1),由 manager 推进选中切片;无选中则回退默认缩放。
|
||||||
|
// 保留 TrackballCamera 的相机拖动/缩放等基础交互(仅在命中/有选中切片时改写行为)。
|
||||||
|
//
|
||||||
|
// 回调由 InteractionManager 注入(render 层不认业务,只发"命中点/双击/滚轮"事件)。
|
||||||
|
class PickInteractorStyle : public vtkInteractorStyleTrackballCamera {
|
||||||
|
public:
|
||||||
|
static PickInteractorStyle* New();
|
||||||
|
vtkTypeMacro(PickInteractorStyle, vtkInteractorStyleTrackballCamera);
|
||||||
|
|
||||||
|
// 单击命中世界点(已命中某 prop)。用于设焦点+选中切片。
|
||||||
|
std::function<void(const Vec3& worldPoint)> onPick;
|
||||||
|
// 双击世界点。用于正视所在切片。
|
||||||
|
std::function<void(const Vec3& worldPoint)> onDoubleClick;
|
||||||
|
// 滚轮步进:dir=+1 前/-1 后。返回 true 表示已被消费(有选中切片推进),
|
||||||
|
// false 则执行默认相机缩放。
|
||||||
|
std::function<bool(int dir)> onWheelStep;
|
||||||
|
|
||||||
|
void OnLeftButtonDown() override;
|
||||||
|
void OnMouseWheelForward() override;
|
||||||
|
void OnMouseWheelBackward() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
PickInteractorStyle() = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 在当前鼠标位置拾取世界点;命中返回 true 并填 out。
|
||||||
|
bool pickWorld(Vec3& out);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 法向接近竖直(±Z)时 viewUp 不能再取"向上",退备用 up。
|
||||||
|
constexpr double kVerticalThreshold = 0.999;
|
||||||
|
constexpr double kSqrt2Inv = 0.70710678118654752440; // sin/cos 45°
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
double dot(const Vec3& a, const Vec3& b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
|
||||||
|
|
||||||
|
double norm(const Vec3& a) { return std::sqrt(dot(a, a)); }
|
||||||
|
|
||||||
|
Vec3 normalize(const Vec3& a) {
|
||||||
|
const double n = norm(a);
|
||||||
|
if (n <= 0.0) return {0.0, 0.0, 1.0}; // 零向量兜底
|
||||||
|
return {a[0] / n, a[1] / n, a[2] / n};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 cross(const Vec3& a, const Vec3& b) {
|
||||||
|
return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 axisNormal(SliceAxis axis) {
|
||||||
|
switch (axis) {
|
||||||
|
case SliceAxis::UpDown: return {0.0, 0.0, 1.0};
|
||||||
|
case SliceAxis::FrontBack: return {0.0, 1.0, 0.0};
|
||||||
|
case SliceAxis::LeftRight: return {1.0, 0.0, 0.0};
|
||||||
|
case SliceAxis::Oblique: return {kSqrt2Inv, 0.0, kSqrt2Inv};
|
||||||
|
}
|
||||||
|
return {0.0, 0.0, 1.0};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 boundsCenter(const std::array<double, 6>& b) {
|
||||||
|
return {0.5 * (b[0] + b[1]), 0.5 * (b[2] + b[3]), 0.5 * (b[4] + b[5])};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 advanceOrigin(const Vec3& origin, const Vec3& normal, double step) {
|
||||||
|
const Vec3 n = normalize(normal);
|
||||||
|
return {origin[0] + n[0] * step, origin[1] + n[1] * step, origin[2] + n[2] * step};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 clampToBounds(const Vec3& origin, const std::array<double, 6>& b) {
|
||||||
|
auto clamp1 = [](double v, double lo, double hi) {
|
||||||
|
if (lo > hi) std::swap(lo, hi); // 容错:bounds 反序
|
||||||
|
if (v < lo) return lo;
|
||||||
|
if (v > hi) return hi;
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
return {clamp1(origin[0], b[0], b[1]), clamp1(origin[1], b[2], b[3]),
|
||||||
|
clamp1(origin[2], b[4], b[5])};
|
||||||
|
}
|
||||||
|
|
||||||
|
double wheelStep(const std::array<double, 6>& b, int dir) {
|
||||||
|
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 mag = diag * 0.02; // 一次滚轮 ≈ 1/50 对角线
|
||||||
|
return (dir >= 0 ? mag : -mag);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nearestPlane(const std::vector<Vec3>& centers, const std::vector<Vec3>& normals,
|
||||||
|
const Vec3& p) {
|
||||||
|
int best = -1;
|
||||||
|
double bestDist = 0.0;
|
||||||
|
for (std::size_t i = 0; i < centers.size() && i < normals.size(); ++i) {
|
||||||
|
const Vec3 n = normalize(normals[i]);
|
||||||
|
const Vec3 d{p[0] - centers[i][0], p[1] - centers[i][1], p[2] - centers[i][2]};
|
||||||
|
const double dist = std::abs(dot(d, n));
|
||||||
|
if (best < 0 || dist < bestDist) {
|
||||||
|
best = static_cast<int>(i);
|
||||||
|
bestDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
FaceOnCamera faceOnCamera(const Vec3& focal, const Vec3& normal, double dist) {
|
||||||
|
const Vec3 n = normalize(normal);
|
||||||
|
// 相机沿法向退 dist:视线 = focal - position = -n(正对切面)。
|
||||||
|
const Vec3 position{focal[0] + n[0] * dist, focal[1] + n[1] * dist, focal[2] + n[2] * dist};
|
||||||
|
|
||||||
|
// viewUp:取与法向正交、尽量指向 +Z 的向量。
|
||||||
|
// worldUp×n 得右向量,再 n×right 得位于切面内且偏上的 up。
|
||||||
|
// 法向接近竖直(±Z)时 worldUp 与 n 共线 → 退备用 up=+Y。
|
||||||
|
Vec3 worldUp = (std::abs(n[2]) > kVerticalThreshold) ? Vec3{0.0, 1.0, 0.0} : Vec3{0.0, 0.0, 1.0};
|
||||||
|
const Vec3 right = normalize(cross(worldUp, n));
|
||||||
|
const Vec3 up = normalize(cross(n, right));
|
||||||
|
return {position, up};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#pragma once
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
// 三维向量别名(世界系;x=East,y=North,z=-depth*VE)。
|
||||||
|
using Vec3 = std::array<double, 3>;
|
||||||
|
|
||||||
|
// 轴向切片方向(spec §4 F22–F24):
|
||||||
|
// UpDown 上下 = 水平面,法向沿 Z((0,0,1))—— 切出"水平剖面"。
|
||||||
|
// FrontBack 前后 = 法向沿 Y((0,1,0))。
|
||||||
|
// LeftRight 左右 = 法向沿 X((1,0,0))。
|
||||||
|
// Oblique 任意(F25)= 初始 45°,可旋转。
|
||||||
|
enum class SliceAxis { UpDown, FrontBack, LeftRight, Oblique };
|
||||||
|
|
||||||
|
// ── 纯几何函数(无 VTK 依赖,可单测)────────────────────────────────────
|
||||||
|
|
||||||
|
// 轴向/任意切片的初始法向(单位向量)。
|
||||||
|
// UpDown→(0,0,1);FrontBack→(0,1,0);LeftRight→(1,0,0);
|
||||||
|
// Oblique→ XZ 平面内 45°((sin45,0,cos45)),即斜插体的对角面。
|
||||||
|
Vec3 axisNormal(SliceAxis axis);
|
||||||
|
|
||||||
|
// 包围盒 [xmin,xmax,ymin,ymax,zmin,zmax] 的中心点。
|
||||||
|
Vec3 boundsCenter(const std::array<double, 6>& bounds);
|
||||||
|
|
||||||
|
// 滚轮推进:origin' = origin + normal * step(沿法向平移切面一点)。
|
||||||
|
// step>0 正向(沿法向),step<0 反向。
|
||||||
|
Vec3 advanceOrigin(const Vec3& origin, const Vec3& normal, double step);
|
||||||
|
|
||||||
|
// 把 origin 夹在包围盒内(沿法向推进时防切面跑出体外)。
|
||||||
|
// 逐分量 clamp 到 [min,max];退化轴(min==max)取该值。
|
||||||
|
Vec3 clampToBounds(const Vec3& origin, const std::array<double, 6>& bounds);
|
||||||
|
|
||||||
|
// 双击正视:给定切面中心 focal、法向 normal、相机到焦点距离 dist,
|
||||||
|
// 求相机 position 与 viewUp,使相机正对切面(视线 = -normal)。
|
||||||
|
// position = focal + normalize(normal) * dist。
|
||||||
|
// viewUp 取与法向正交的"尽量向上(+Z)"向量;当法向接近竖直(±Z)时
|
||||||
|
// 退到备用 up=+Y 兜底(避免 viewUp 与视线共线导致相机退化)。
|
||||||
|
struct FaceOnCamera {
|
||||||
|
Vec3 position;
|
||||||
|
Vec3 viewUp;
|
||||||
|
};
|
||||||
|
FaceOnCamera faceOnCamera(const Vec3& focal, const Vec3& normal, double dist);
|
||||||
|
|
||||||
|
// 滚轮推进步长:取包围盒对角线长度的固定比例 × 方向(±1)。
|
||||||
|
// 使一次滚轮在体内移动适中(约 1/50 对角线);dir>0 沿法向、dir<0 反向。
|
||||||
|
double wheelStep(const std::array<double, 6>& bounds, int dir);
|
||||||
|
|
||||||
|
// 在切片中心列表中找离世界点最近的索引(按到平面的距离最小)。
|
||||||
|
// centers/normals 等长;空列表返回 -1。worldPoint 在哪张切片上→该索引。
|
||||||
|
int nearestPlane(const std::vector<Vec3>& centers, const std::vector<Vec3>& normals,
|
||||||
|
const Vec3& worldPoint);
|
||||||
|
|
||||||
|
// 向量工具(暴露供测试/复用)。
|
||||||
|
double dot(const Vec3& a, const Vec3& b);
|
||||||
|
double norm(const Vec3& a);
|
||||||
|
Vec3 normalize(const Vec3& a); // 零向量返回 (0,0,1) 兜底
|
||||||
|
Vec3 cross(const Vec3& a, const Vec3& b);
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
#include "interact/SliceTool.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include <vtkImageData.h>
|
||||||
|
#include <vtkImagePlaneWidget.h>
|
||||||
|
#include <vtkLookupTable.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
|
#include <vtkTrivialProducer.h>
|
||||||
|
|
||||||
|
#include "ColorLutBuilder.hpp"
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 任意切片初始法向(45°,XZ 面内);轴向用 SetPlaneOrientationTo*。
|
||||||
|
constexpr double kSqrt2Inv = 0.70710678118654752440;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
|
||||||
|
const geopro::core::ColorScale& cs, double vmin, double vmax)
|
||||||
|
: axis_(axis), image_(image), widget_(vtkSmartPointer<vtkImagePlaneWidget>::New()) {
|
||||||
|
// 经 trivial producer 把已存在的 vtkImageData 接入 widget(widget 只暴露 SetInputConnection)。
|
||||||
|
// producer_ 为成员,随 SliceTool 保活(局部变量会构造后即析构→管线断裂,评审 H1)。
|
||||||
|
producer_ = vtkSmartPointer<vtkTrivialProducer>::New();
|
||||||
|
producer_->SetOutput(image_);
|
||||||
|
widget_->SetInputConnection(producer_->GetOutputPort());
|
||||||
|
|
||||||
|
widget_->SetInteractor(interactor);
|
||||||
|
widget_->RestrictPlaneToVolumeOn(); // 切面限制在体内,滚轮推进不跑飞
|
||||||
|
widget_->SetResliceInterpolateToLinear(); // reslice 线性插值出连续剖面(非 cutter 交线)
|
||||||
|
widget_->TextureInterpolateOn();
|
||||||
|
widget_->DisplayTextOff();
|
||||||
|
|
||||||
|
// 色阶 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widget_->On();
|
||||||
|
}
|
||||||
|
|
||||||
|
SliceTool::~SliceTool() { close(); }
|
||||||
|
|
||||||
|
std::array<double, 6> SliceTool::imageBounds() const {
|
||||||
|
std::array<double, 6> b{{0, 0, 0, 0, 0, 0}};
|
||||||
|
if (image_) image_->GetBounds(b.data());
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 SliceTool::normal() const {
|
||||||
|
double n[3] = {0, 0, 1};
|
||||||
|
if (widget_) widget_->GetNormal(n);
|
||||||
|
return normalize({n[0], n[1], n[2]});
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 SliceTool::center() const {
|
||||||
|
double c[3] = {0, 0, 0};
|
||||||
|
if (widget_) widget_->GetCenter(c);
|
||||||
|
return {c[0], c[1], c[2]};
|
||||||
|
}
|
||||||
|
|
||||||
|
void SliceTool::advance(double step) {
|
||||||
|
if (!widget_) return;
|
||||||
|
// 沿法向刚性平移整张切面:origin/point1/point2 同步加 normal*step。只移 origin 会让
|
||||||
|
// 面内两端点不动→平面变形/脱轴(评审 M1)。RestrictPlaneToVolumeOn 负责夹在体内。
|
||||||
|
const Vec3 n = normal();
|
||||||
|
const double d[3] = {n[0] * step, n[1] * step, n[2] * step};
|
||||||
|
double o[3], p1[3], p2[3];
|
||||||
|
widget_->GetOrigin(o);
|
||||||
|
widget_->GetPoint1(p1);
|
||||||
|
widget_->GetPoint2(p2);
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
o[i] += d[i];
|
||||||
|
p1[i] += d[i];
|
||||||
|
p2[i] += d[i];
|
||||||
|
}
|
||||||
|
widget_->SetOrigin(o);
|
||||||
|
widget_->SetPoint1(p1);
|
||||||
|
widget_->SetPoint2(p2);
|
||||||
|
widget_->UpdatePlacement();
|
||||||
|
}
|
||||||
|
|
||||||
|
double SliceTool::distanceToPlane(const Vec3& p) const {
|
||||||
|
const Vec3 c = center();
|
||||||
|
const Vec3 n = normal();
|
||||||
|
return std::abs(dot({p[0] - c[0], p[1] - c[1], p[2] - c[2]}, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SliceTool::close() {
|
||||||
|
if (!widget_) return;
|
||||||
|
widget_->Off();
|
||||||
|
widget_->SetInteractor(nullptr); // 解除观察者,防悬挂崩溃
|
||||||
|
widget_ = nullptr; // 置空 → 二次 close()/析构真正幂等(不再 Off 已解绑 widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
#pragma once
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
|
||||||
|
class vtkImageData;
|
||||||
|
class vtkImagePlaneWidget;
|
||||||
|
class vtkRenderWindowInteractor;
|
||||||
|
class vtkTrivialProducer;
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
// 单个切片工具:封装 vtkImagePlaneWidget。
|
||||||
|
// 内部对体素 vtkImageData 做 reslice + 纹理着色(spec §9.1 钉死 reslice 路线,非 cutter)。
|
||||||
|
// 轴向(UpDown/FrontBack/LeftRight):SetPlaneOrientationToX/Y/Z,角度固定。
|
||||||
|
// 任意(Oblique):设初始 45° 法向,允许旋转。
|
||||||
|
// 套上调用方提供的色阶 LUT(ColorLutBuilder)。
|
||||||
|
//
|
||||||
|
// 生命周期:构造即 SetInteractor + On()(须传活的 interactor)。
|
||||||
|
// 析构(或 close())时 Off(),由 vtkSmartPointer 释放,避免悬挂观察者崩溃。
|
||||||
|
// 仅三维视图使用;切到二维由 InteractionManager 统一 close。
|
||||||
|
class SliceTool {
|
||||||
|
public:
|
||||||
|
// image:体素管线产物(含 VE 烤入的 origin/spacing)。interactor:QVTK 的活 interactor。
|
||||||
|
// axis:切面方向。vmin/vmax:色阶区间。
|
||||||
|
SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
|
||||||
|
const geopro::core::ColorScale& cs, double vmin, double vmax);
|
||||||
|
~SliceTool();
|
||||||
|
|
||||||
|
SliceTool(const SliceTool&) = delete;
|
||||||
|
SliceTool& operator=(const SliceTool&) = delete;
|
||||||
|
SliceTool(SliceTool&&) = delete; // 持 VTK widget 观察者,禁移动(仅经 unique_ptr 间接持有)
|
||||||
|
SliceTool& operator=(SliceTool&&) = delete;
|
||||||
|
|
||||||
|
SliceAxis axis() const { return axis_; }
|
||||||
|
|
||||||
|
// 当前切面法向(世界系单位向量)。
|
||||||
|
Vec3 normal() const;
|
||||||
|
// 当前切面中心(origin)。
|
||||||
|
Vec3 center() const;
|
||||||
|
|
||||||
|
// 沿法向推进切面(滚轮,D46):origin += normal*step,夹在 image 包围盒内。
|
||||||
|
void advance(double step);
|
||||||
|
|
||||||
|
// 世界点到本切面(无限平面)的垂直距离绝对值。供 picker 命中判定"点在哪张切片上"。
|
||||||
|
double distanceToPlane(const Vec3& worldPoint) const;
|
||||||
|
|
||||||
|
// 关闭:Off() 并解除 interactor 绑定(幂等)。
|
||||||
|
void close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
SliceAxis axis_;
|
||||||
|
vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证
|
||||||
|
// 把已存在的 image 接入 widget 的 producer:须随 SliceTool 保活(否则构造后析构→管线断裂崩溃,评审 H1)。
|
||||||
|
vtkSmartPointer<vtkTrivialProducer> producer_;
|
||||||
|
vtkSmartPointer<vtkImagePlaneWidget> widget_;
|
||||||
|
|
||||||
|
std::array<double, 6> imageBounds() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -100,6 +100,8 @@ target_sources(geopro_tests PRIVATE render/test_terrain.cpp)
|
||||||
target_sources(geopro_tests PRIVATE render/test_camera_preset.cpp)
|
target_sources(geopro_tests PRIVATE render/test_camera_preset.cpp)
|
||||||
# AxesActor(P2):buildAxes(bounds+unit/mode→vtkCubeAxesActor) 单位换算(英尺/经纬度)/不显示返回空。
|
# AxesActor(P2):buildAxes(bounds+unit/mode→vtkCubeAxesActor) 单位换算(英尺/经纬度)/不显示返回空。
|
||||||
target_sources(geopro_tests PRIVATE render/test_axes.cpp)
|
target_sources(geopro_tests PRIVATE render/test_axes.cpp)
|
||||||
|
# SlicePlaneMath(P3):切面法向/滚轮平移+夹限/双击正视相机(含竖直兜底)/滚轮步长/最近切片——纯几何。
|
||||||
|
target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::render::interact;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void expectVec(const Vec3& a, double x, double y, double z, double eps = 1e-9) {
|
||||||
|
EXPECT_NEAR(a[0], x, eps);
|
||||||
|
EXPECT_NEAR(a[1], y, eps);
|
||||||
|
EXPECT_NEAR(a[2], z, eps);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ── axisNormal:轴向法向(spec F22–F24)+ 任意 45°(F25)──
|
||||||
|
TEST(SlicePlaneMath, AxisNormalUpDownIsZ) { expectVec(axisNormal(SliceAxis::UpDown), 0, 0, 1); }
|
||||||
|
TEST(SlicePlaneMath, AxisNormalFrontBackIsY) { expectVec(axisNormal(SliceAxis::FrontBack), 0, 1, 0); }
|
||||||
|
TEST(SlicePlaneMath, AxisNormalLeftRightIsX) { expectVec(axisNormal(SliceAxis::LeftRight), 1, 0, 0); }
|
||||||
|
TEST(SlicePlaneMath, AxisNormalObliqueIs45) {
|
||||||
|
const auto n = axisNormal(SliceAxis::Oblique);
|
||||||
|
const double s = std::sqrt(0.5);
|
||||||
|
expectVec(n, s, 0, s);
|
||||||
|
EXPECT_NEAR(norm(n), 1.0, 1e-9); // 单位向量
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── boundsCenter ──
|
||||||
|
TEST(SlicePlaneMath, BoundsCenter) {
|
||||||
|
expectVec(boundsCenter({0, 10, -4, 4, 0, 6}), 5, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── advanceOrigin:沿法向平移(滚轮推进,D46)──
|
||||||
|
TEST(SlicePlaneMath, AdvanceAlongZ) {
|
||||||
|
expectVec(advanceOrigin({1, 2, 3}, {0, 0, 1}, 5.0), 1, 2, 8);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, AdvanceBackward) {
|
||||||
|
expectVec(advanceOrigin({1, 2, 3}, {0, 0, 1}, -2.0), 1, 2, 1);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, AdvanceNormalizesDirection) {
|
||||||
|
// 非单位法向:先归一化再推进,步长为世界距离。
|
||||||
|
expectVec(advanceOrigin({0, 0, 0}, {0, 0, 5}, 3.0), 0, 0, 3);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, AdvanceObliqueMovesAlong45) {
|
||||||
|
const auto o = advanceOrigin({0, 0, 0}, {1, 0, 1}, std::sqrt(2.0));
|
||||||
|
expectVec(o, 1, 0, 1); // 沿 45° 推进 √2 → (1,0,1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── clampToBounds:推进出体外被夹回(滚轮限位)──
|
||||||
|
TEST(SlicePlaneMath, ClampInsideUnchanged) {
|
||||||
|
expectVec(clampToBounds({5, 0, 3}, {0, 10, -4, 4, 0, 6}), 5, 0, 3);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, ClampOutsideHigh) {
|
||||||
|
expectVec(clampToBounds({5, 0, 99}, {0, 10, -4, 4, 0, 6}), 5, 0, 6);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, ClampOutsideLow) {
|
||||||
|
expectVec(clampToBounds({-5, 0, -1}, {0, 10, -4, 4, 0, 6}), 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── faceOnCamera:双击正视(E54)──
|
||||||
|
// 法向 +Y:相机退到 focal+Y*dist,视线 = -Y,viewUp = +Z(切面内向上)。
|
||||||
|
TEST(SlicePlaneMath, FaceOnFrontBackNormal) {
|
||||||
|
const auto cam = faceOnCamera({0, 0, 0}, {0, 1, 0}, 10.0);
|
||||||
|
expectVec(cam.position, 0, 10, 0);
|
||||||
|
// viewUp 与法向正交且偏 +Z。
|
||||||
|
EXPECT_NEAR(dot(cam.viewUp, Vec3{0, 1, 0}), 0.0, 1e-9);
|
||||||
|
EXPECT_GT(cam.viewUp[2], 0.5);
|
||||||
|
}
|
||||||
|
// 法向 +X:position=focal+X*dist,viewUp 偏 +Z。
|
||||||
|
TEST(SlicePlaneMath, FaceOnLeftRightNormal) {
|
||||||
|
const auto cam = faceOnCamera({1, 2, 3}, {1, 0, 0}, 5.0);
|
||||||
|
expectVec(cam.position, 6, 2, 3);
|
||||||
|
EXPECT_NEAR(dot(cam.viewUp, Vec3{1, 0, 0}), 0.0, 1e-9);
|
||||||
|
EXPECT_GT(cam.viewUp[2], 0.5);
|
||||||
|
}
|
||||||
|
// 法向竖直 +Z(上下切片):viewUp 不能再取 +Z(与法向共线),兜底取 +Y。
|
||||||
|
TEST(SlicePlaneMath, FaceOnVerticalNormalFallsBackToY) {
|
||||||
|
const auto cam = faceOnCamera({0, 0, 0}, {0, 0, 1}, 8.0);
|
||||||
|
expectVec(cam.position, 0, 0, 8);
|
||||||
|
// viewUp 与法向(+Z)正交(z≈0),且非零。
|
||||||
|
EXPECT_NEAR(cam.viewUp[2], 0.0, 1e-9);
|
||||||
|
EXPECT_NEAR(norm(cam.viewUp), 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
// 法向竖直 -Z 同样兜底。
|
||||||
|
TEST(SlicePlaneMath, FaceOnVerticalDownNormalFallsBack) {
|
||||||
|
const auto cam = faceOnCamera({0, 0, 0}, {0, 0, -1}, 4.0);
|
||||||
|
expectVec(cam.position, 0, 0, -4);
|
||||||
|
EXPECT_NEAR(cam.viewUp[2], 0.0, 1e-9);
|
||||||
|
EXPECT_NEAR(norm(cam.viewUp), 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
// 非单位法向:position 用归一化法向 → 距焦点恰为 dist。
|
||||||
|
TEST(SlicePlaneMath, FaceOnNormalizesNormal) {
|
||||||
|
const auto cam = faceOnCamera({0, 0, 0}, {0, 3, 0}, 6.0);
|
||||||
|
expectVec(cam.position, 0, 6, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── wheelStep:滚轮推进步长(按对角线比例 × 方向)──
|
||||||
|
TEST(SlicePlaneMath, WheelStepForwardPositive) {
|
||||||
|
EXPECT_GT(wheelStep({0, 10, 0, 0, 0, 0}, +1), 0.0);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, WheelStepBackwardNegative) {
|
||||||
|
EXPECT_LT(wheelStep({0, 10, 0, 0, 0, 0}, -1), 0.0);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, WheelStepScalesWithBounds) {
|
||||||
|
const double small = wheelStep({0, 10, 0, 0, 0, 0}, 1);
|
||||||
|
const double big = wheelStep({0, 100, 0, 0, 0, 0}, 1);
|
||||||
|
EXPECT_GT(big, small); // 体越大步长越大
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── nearestPlane:找点所在切片(按到平面距离最小)──
|
||||||
|
TEST(SlicePlaneMath, NearestPlaneEmptyIsMinusOne) {
|
||||||
|
EXPECT_EQ(nearestPlane({}, {}, {0, 0, 0}), -1);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, NearestPlanePicksClosest) {
|
||||||
|
// 两张水平切片 z=0 与 z=10(法向 +Z);点 z=8 → 更近 z=10(索引 1)。
|
||||||
|
std::vector<Vec3> centers{{0, 0, 0}, {0, 0, 10}};
|
||||||
|
std::vector<Vec3> normals{{0, 0, 1}, {0, 0, 1}};
|
||||||
|
EXPECT_EQ(nearestPlane(centers, normals, {5, 5, 8}), 1);
|
||||||
|
EXPECT_EQ(nearestPlane(centers, normals, {5, 5, 2}), 0);
|
||||||
|
}
|
||||||
|
TEST(SlicePlaneMath, NearestPlaneIgnoresInPlaneOffset) {
|
||||||
|
// 单张 z=0 水平面:点无论 x/y 多远,只要 z=0 距离为 0 → 命中。
|
||||||
|
std::vector<Vec3> centers{{0, 0, 0}};
|
||||||
|
std::vector<Vec3> normals{{0, 0, 1}};
|
||||||
|
EXPECT_EQ(nearestPlane(centers, normals, {999, -999, 0}), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 向量工具 ──
|
||||||
|
TEST(SlicePlaneMath, NormalizeZeroFallsBack) { expectVec(normalize({0, 0, 0}), 0, 0, 1); }
|
||||||
|
TEST(SlicePlaneMath, CrossBasic) { expectVec(cross({1, 0, 0}, {0, 1, 0}), 0, 0, 1); }
|
||||||
Loading…
Reference in New Issue