geopro/src/render/interact/SliceTool.cpp

177 lines
7.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 接入 widgetwidget 只暴露 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角度不可调符合 G22G24
// 上下=水平面=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°F25vtkImagePlaneWidget 用 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