#include "interact/SliceTool.hpp" #include #include #include #include #include #include #include #include #include #include #include #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::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::New(); interactObserver_->SetClientData(this); interactObserver_->SetCallback([](vtkObject*, unsigned long, void* client, void*) { auto* self = static_cast(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::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& origin, const std::array& point1, const std::array& point2) : axis_(axis), image_(image), widget_(vtkSmartPointer::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 SliceTool::imageBounds() const { std::array 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 SliceTool::coloredResliceImage() const { if (!widget_) return nullptr; vtkImageMapToColors* cm = widget_->GetColorMap(); // widget 内部把 reslice 经 LUT 上色 → 纹理 if (cm == nullptr) return nullptr; cm->Update(); auto out = vtkSmartPointer::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