177 lines
7.2 KiB
C++
177 lines
7.2 KiB
C++
#include "interact/SliceTool.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
#include <vtkCallbackCommand.h>
|
||
#include <vtkCommand.h>
|
||
#include <vtkImageData.h>
|
||
#include <vtkImagePlaneWidget.h>
|
||
#include <vtkLookupTable.h>
|
||
#include <vtkProperty.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_->SetLeftButtonAction(vtkImagePlaneWidget::VTK_SLICE_MOTION_ACTION);
|
||
widget_->SetMiddleButtonAction(vtkImagePlaneWidget::VTK_CURSOR_ACTION);
|
||
// 旋转只允许"任意切片"(F25 可任意调整);轴向(上下/前后/左右)角度固定(G22-24 角度不能再调整):
|
||
// 把切面边缘(margins, 旋转抓取区)设为 0 → 抓哪里都只移动、不旋转。
|
||
if (axis_ != SliceAxis::Oblique) {
|
||
widget_->SetMarginSizeX(0.0);
|
||
widget_->SetMarginSizeY(0.0);
|
||
}
|
||
|
||
widget_->On();
|
||
// 保持 widget 交互开启:任意切片可拖动调整角度/位置(F25 '可任意调整')。
|
||
// 监听其交互开始事件 → 触碰本切片即回调 onInteract(上层据此设为选中)。
|
||
interactObserver_ = vtkSmartPointer<vtkCallbackCommand>::New();
|
||
interactObserver_->SetClientData(this);
|
||
interactObserver_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
|
||
auto* self = static_cast<SliceTool*>(client);
|
||
if (self && self->onInteract) self->onInteract();
|
||
});
|
||
widget_->AddObserver(vtkCommand::StartInteractionEvent, interactObserver_);
|
||
}
|
||
|
||
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::setSelected(bool sel) {
|
||
if (!widget_) return;
|
||
// 切片边框 = widget 的 PlaneProperty:选中→亮黄粗线,未选中→暗灰细线。
|
||
if (auto* prop = widget_->GetPlaneProperty()) {
|
||
if (sel) {
|
||
prop->SetColor(0.0, 0.95, 1.0); // 亮青:与未选的暗灰强对比
|
||
prop->SetLineWidth(3.5);
|
||
} else {
|
||
prop->SetColor(0.35, 0.35, 0.4); // 暗灰
|
||
prop->SetLineWidth(1.0);
|
||
}
|
||
}
|
||
}
|
||
|
||
void SliceTool::close() {
|
||
if (!widget_) return;
|
||
onInteract = nullptr; // 先断业务回调,避免 Off 期间触发到上层
|
||
if (interactObserver_) {
|
||
widget_->RemoveObserver(interactObserver_);
|
||
interactObserver_ = nullptr;
|
||
}
|
||
widget_->Off();
|
||
widget_->SetInteractor(nullptr); // 解除观察者,防悬挂崩溃
|
||
widget_ = nullptr; // 置空 → 二次 close()/析构真正幂等(不再 Off 已解绑 widget)
|
||
}
|
||
|
||
} // namespace geopro::render::interact
|