228 lines
9.1 KiB
C++
228 lines
9.1 KiB
C++
#include "interact/SliceTool.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
#include <vtkCallbackCommand.h>
|
||
#include <vtkCommand.h>
|
||
#include <vtkImageData.h>
|
||
#include <vtkImageMapToColors.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
|
||
|
||
void SliceTool::initWidget(const geopro::core::ColorScale& cs, double vmin, double vmax) {
|
||
// 经 trivial producer 把已存在的 vtkImageData 接入 widget(widget 只暴露 SetInputConnection)。
|
||
// producer_ 为成员,随 SliceTool 保活(局部变量会构造后即析构→管线断裂,评审 H1)。
|
||
producer_ = vtkSmartPointer<vtkTrivialProducer>::New();
|
||
producer_->SetOutput(image_);
|
||
widget_->SetInputConnection(producer_->GetOutputPort());
|
||
|
||
widget_->RestrictPlaneToVolumeOn(); // 切面限制在体内,滚轮推进不跑飞
|
||
widget_->SetResliceInterpolateToLinear(); // reslice 线性插值出连续剖面(非 cutter 交线)
|
||
widget_->TextureInterpolateOn();
|
||
widget_->DisplayTextOff();
|
||
|
||
// 色阶 LUT 套用:用户自管 LUT(不让 widget 用默认灰度窗位)。
|
||
auto lut = buildLut(cs, vmin, vmax);
|
||
widget_->SetLookupTable(lut);
|
||
}
|
||
|
||
void SliceTool::applyMarginsAndActivate() {
|
||
// 左键拖动=移动切面(默认左键是窗位调整,无用);中键=取值光标。
|
||
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();
|
||
// 监听其交互开始事件 → 触碰本切片即回调 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(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
|
||
const geopro::core::ColorScale& cs, double vmin, double vmax)
|
||
: axis_(axis), image_(image), widget_(vtkSmartPointer<vtkImagePlaneWidget>::New()) {
|
||
initWidget(cs, vmin, vmax);
|
||
widget_->SetInteractor(interactor);
|
||
|
||
// 轴向:固定到 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):用 Origin/Point1/Point2 三点定义平面。法向 (sin45,0,cos45):
|
||
// in-plane 轴1=Y(0,1,0),轴2=(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]);
|
||
const double hxz = 0.5 * std::max(b[1] - b[0], b[5] - b[4]);
|
||
const double a2x = kSqrt2Inv, a2z = -kSqrt2Inv;
|
||
const double ox = cx - a2x * hxz;
|
||
const double oy = cy - hy;
|
||
const double oz = cz - a2z * hxz;
|
||
widget_->SetOrigin(ox, oy, oz);
|
||
widget_->SetPoint1(ox, oy + 2.0 * hy, oz); // 沿 +Y
|
||
widget_->SetPoint2(ox + a2x * 2.0 * hxz, oy, oz + a2z * 2.0 * hxz); // 沿 (cos45,0,-sin45)
|
||
widget_->UpdatePlacement();
|
||
break;
|
||
}
|
||
}
|
||
applyMarginsAndActivate();
|
||
}
|
||
|
||
SliceTool::SliceTool(vtkImageData* image, vtkRenderWindowInteractor* interactor, SliceAxis axis,
|
||
const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||
const std::array<double, 3>& origin, const std::array<double, 3>& point1,
|
||
const std::array<double, 3>& point2)
|
||
: axis_(axis), image_(image), widget_(vtkSmartPointer<vtkImagePlaneWidget>::New()) {
|
||
initWidget(cs, vmin, vmax);
|
||
widget_->SetInteractor(interactor);
|
||
// 还原:直接用保存的精确三点(不做轴向 snap),保证尺寸/朝向/位置与保存时一致。
|
||
widget_->SetOrigin(origin[0], origin[1], origin[2]);
|
||
widget_->SetPoint1(point1[0], point1[1], point1[2]);
|
||
widget_->SetPoint2(point2[0], point2[1], point2[2]);
|
||
widget_->UpdatePlacement();
|
||
applyMarginsAndActivate(); // 按 axis 锁旋转(轴向切片仍不可旋转)
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
void SliceTool::planePoints(double origin[3], double point1[3], double point2[3]) const {
|
||
if (!widget_) {
|
||
for (int i = 0; i < 3; ++i) origin[i] = point1[i] = point2[i] = 0.0;
|
||
return;
|
||
}
|
||
widget_->GetOrigin(origin);
|
||
widget_->GetPoint1(point1);
|
||
widget_->GetPoint2(point2);
|
||
}
|
||
|
||
vtkImageData* SliceTool::reslicedOutput() const {
|
||
return widget_ ? widget_->GetResliceOutput() : nullptr;
|
||
}
|
||
|
||
void SliceTool::setInteractive(bool on) {
|
||
interactive_ = on; // 记录锁定态:setVisible 重显时复原
|
||
if (widget_) widget_->SetInteraction(on ? 1 : 0); // 关=锁移动/旋转/光标,纹理仍显示
|
||
}
|
||
|
||
void SliceTool::setVisible(bool on) {
|
||
if (!widget_) return;
|
||
widget_->SetEnabled(on ? 1 : 0); // 翻显隐(不销毁):几何/纹理保留、切回零重建
|
||
if (on) widget_->SetInteraction(interactive_ ? 1 : 0); // SetEnabled 可能重置交互→复原锁定态
|
||
}
|
||
|
||
vtkSmartPointer<vtkImageData> SliceTool::coloredResliceImage() const {
|
||
if (!widget_) return nullptr;
|
||
vtkImageMapToColors* cm = widget_->GetColorMap(); // widget 内部把 reslice 经 LUT 上色 → 纹理
|
||
if (cm == nullptr) return nullptr;
|
||
cm->Update();
|
||
auto out = vtkSmartPointer<vtkImageData>::New();
|
||
out->DeepCopy(cm->GetOutput()); // 即屏幕切片所贴像素(RGBA, 外区 alpha=0)
|
||
return out;
|
||
}
|
||
|
||
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
|