#include "interact/SliceTool.hpp" #include #include #include #include #include #include #include #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::New()) { // 经 trivial producer 把已存在的 vtkImageData 接入 widget(widget 只暴露 SetInputConnection)。 // producer_ 为成员,随 SliceTool 保活(局部变量会构造后即析构→管线断裂,评审 H1)。 producer_ = vtkSmartPointer::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(); // 关闭 widget 自身的鼠标交互(窗位/光标/拖动):否则它会"吃掉"落在切片面上的左键, // 自定义 PickInteractorStyle 收不到 → 单击选中/双击正视/绕点旋转全失效(实测根因)。 // 关掉后切片仍正常显示,点击穿透到样式;切面移动改由滚轮(advance)驱动。 widget_->InteractionOff(); } 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(); } 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