#include "SliceViewWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { vtkSmartPointer makeCrossActor(const double rgb[3]) { auto poly = vtkSmartPointer::New(); auto pts = vtkSmartPointer::New(); pts->InsertNextPoint(0, 0, 0); pts->InsertNextPoint(1, 0, 0); auto lines = vtkSmartPointer::New(); lines->InsertNextCell(2); lines->InsertCellPoint(0); lines->InsertCellPoint(1); poly->SetPoints(pts); poly->SetLines(lines); auto mapper = vtkSmartPointer::New(); mapper->SetInputData(poly); auto actor = vtkSmartPointer::New(); actor->SetMapper(mapper); actor->GetProperty()->SetColor(rgb[0], rgb[1], rgb[2]); actor->GetProperty()->SetLineWidth(1.8); return actor; } QLabel* makeEdgeLabel(QWidget* parent, const QString& text, const QColor& color) { auto* lab = new QLabel(text, parent); lab->setStyleSheet(QStringLiteral( "QLabel { color: %1; font: bold 14px 'Segoe UI'; background: transparent; }") .arg(color.name())); lab->setAttribute(Qt::WA_TransparentForMouseEvents); lab->raise(); return lab; } } // namespace ViewAxis SliceViewWidget::colorForLineDirection(const double dir[3]) { int ax = 0; if (std::abs(dir[1]) >= std::abs(dir[0]) && std::abs(dir[1]) >= std::abs(dir[2])) ax = 1; else if (std::abs(dir[2]) >= std::abs(dir[0]) && std::abs(dir[2]) >= std::abs(dir[1])) ax = 2; if (ax == 0) return ViewAxis::Sagittal; if (ax == 1) return ViewAxis::Coronal; return ViewAxis::Axial; } SliceViewWidget::SliceViewWidget(ViewAxis axis, QWidget* parent) : QWidget(parent) , m_axis(axis) { // 必须开 StyledBackground,否则 stylesheet 边框经常不画 setAttribute(Qt::WA_StyledBackground, true); setAutoFillBackground(true); applyBorderStyle(); auto* layout = new QVBoxLayout(this); // 给色框留出空隙(VTK OpenGL 子窗体会盖住无 margin 的边) layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(0); m_vtkWidget = new QVTKOpenGLWidget(this); layout->addWidget(m_vtkWidget); auto window = vtkSmartPointer::New(); m_vtkWidget->SetRenderWindow(window); m_renderer = vtkSmartPointer::New(); m_renderer->SetBackground(0.08, 0.08, 0.1); window->AddRenderer(m_renderer); m_lutBg = vtkSmartPointer::New(); m_lutFg = vtkSmartPointer::New(); m_mapBg = vtkSmartPointer::New(); m_mapFg = vtkSmartPointer::New(); m_actorBg = vtkSmartPointer::New(); m_actorFg = vtkSmartPointer::New(); rebuildColorMaps(); double rgbH[3], rgbV[3]; axisColorRgb(ViewAxis::Axial, rgbH); axisColorRgb(ViewAxis::Sagittal, rgbV); m_crossH = makeCrossActor(rgbH); m_crossV = makeCrossActor(rgbV); m_renderer->AddActor2D(m_crossH); m_renderer->AddActor2D(m_crossV); const QColor ac = axisColor(m_axis); QString top, bottom, left, right; switch (m_axis) { case ViewAxis::Axial: top = QStringLiteral("A"); bottom = QStringLiteral("P"); left = QStringLiteral("L"); right = QStringLiteral("R"); break; case ViewAxis::Sagittal: top = QStringLiteral("S"); bottom = QStringLiteral("I"); left = QStringLiteral("P"); right = QStringLiteral("A"); break; case ViewAxis::Coronal: top = QStringLiteral("S"); bottom = QStringLiteral("I"); left = QStringLiteral("L"); right = QStringLiteral("R"); break; } m_labelTop = makeEdgeLabel(this, top, ac); m_labelBottom = makeEdgeLabel(this, bottom, ac); m_labelLeft = makeEdgeLabel(this, left, ac); m_labelRight = makeEdgeLabel(this, right, ac); m_pickLabel = new QLabel(this); m_pickLabel->setStyleSheet(QStringLiteral( "QLabel { color: #f0f0f0; font: 12px 'Consolas'; background: rgba(0,0,0,140);" " padding: 4px 6px; border-radius: 3px; }")); m_pickLabel->setAttribute(Qt::WA_TransparentForMouseEvents); m_pickLabel->hide(); m_pickLabel->raise(); auto style = vtkSmartPointer::New(); m_vtkWidget->GetInteractor()->SetInteractorStyle(style); m_vtkWidget->installEventFilter(this); m_vtkWidget->setMouseTracking(true); setMouseTracking(true); updateOrientationLabels(); } void SliceViewWidget::applyBorderStyle() { const QColor c = axisColor(m_axis); QPalette pal = palette(); pal.setColor(QPalette::Window, c); setPalette(pal); setStyleSheet(QStringLiteral( "SliceViewWidget {" " border: 1px solid %1;" " background-color: %1;" "}") .arg(c.name())); } void SliceViewWidget::paintEvent(QPaintEvent* event) { QWidget::paintEvent(event); QPainter p(this); p.setRenderHint(QPainter::Antialiasing, false); p.setPen(QPen(axisColor(m_axis), 1)); p.setBrush(Qt::NoBrush); p.drawRect(rect().adjusted(0, 0, -1, -1)); } namespace { void fillLookupTable(vtkLookupTable* lut, DoseColorMap map, double lo, double hi) { if (!lut) return; if (!(hi > lo)) hi = lo + 1.0; lut->SetNumberOfTableValues(256); lut->SetRange(lo, hi); auto clamp01 = [](double x) { return std::max(0.0, std::min(1.0, x)); }; for (int i = 0; i < 256; ++i) { const double t = i / 255.0; double r = t, g = t, b = t; switch (map) { case DoseColorMap::Gray: r = g = b = t; break; case DoseColorMap::Rainbow: { // 蓝→青→绿→黄→红 const double h = (1.0 - t) * 0.6667; const double s = 1.0; const double v = 1.0; const int sector = static_cast(h * 6.0) % 6; const double f = h * 6.0 - std::floor(h * 6.0); const double p = v * (1.0 - s); const double q = v * (1.0 - s * f); const double u = v * (1.0 - s * (1.0 - f)); switch (sector) { case 0: r = v; g = u; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = u; break; case 3: r = p; g = q; b = v; break; case 4: r = u; g = p; b = v; break; default: r = v; g = p; b = q; break; } break; } case DoseColorMap::Jet: { // 经典 jet r = clamp01(1.5 - std::abs(4.0 * t - 3.0)); g = clamp01(1.5 - std::abs(4.0 * t - 2.0)); b = clamp01(1.5 - std::abs(4.0 * t - 1.0)); break; } case DoseColorMap::Hot: { r = clamp01(t * 2.5); g = clamp01(t * 2.5 - 1.0); b = clamp01(t * 2.5 - 2.0); break; } } lut->SetTableValue(i, r, g, b, 1.0); } lut->Build(); } } // namespace void SliceViewWidget::rebuildColorMaps() { const double lo = m_level - 0.5 * m_window; const double hi = m_level + 0.5 * m_window; fillLookupTable(m_lutBg, m_colorMap, lo, hi); fillLookupTable(m_lutFg, m_colorMap, lo, hi); if (m_mapBg) m_mapBg->SetLookupTable(m_lutBg); if (m_mapFg) m_mapFg->SetLookupTable(m_lutFg); } void SliceViewWidget::refreshMappedImages() { // 无切片输入时禁止 Update,否则 vtkImageMapToColors 会报 0 connections if (m_bgSlice && m_mapBg) { m_mapBg->SetInputData(m_bgSlice); m_mapBg->SetLookupTable(m_lutBg); m_mapBg->SetOutputFormatToRGB(); m_mapBg->Update(); if (auto* mapper = m_actorBg->GetMapper()) mapper->SetInputConnection(m_mapBg->GetOutputPort()); } if (m_fgSlice && m_mapFg) { m_mapFg->SetInputData(m_fgSlice); m_mapFg->SetLookupTable(m_lutFg); m_mapFg->SetOutputFormatToRGB(); m_mapFg->Update(); if (auto* mapper = m_actorFg->GetMapper()) mapper->SetInputConnection(m_mapFg->GetOutputPort()); } if (m_bgSlice && m_vtkWidget && m_vtkWidget->GetRenderWindow()) m_vtkWidget->GetRenderWindow()->Render(); } void SliceViewWidget::setColorMap(DoseColorMap map) { m_colorMap = map; rebuildColorMaps(); refreshMappedImages(); } void SliceViewWidget::viewDirections(double u[3], double v[3], double n[3]) const { // 物理轴按 LPS 显示(+X=L +Y=P +Z=S)。MHD 虽写 RAI,但按 LPS 才能上A下P。 // 轴位:上A下P左L右R —— 「屁股朝天」= P 在上,必须用 v=-Y(A) for (int i = 0; i < 3; ++i) u[i] = v[i] = n[i] = 0.0; switch (m_axis) { case ViewAxis::Axial: u[0] = -1.0; // 屏右 = R = -X v[1] = -1.0; // 屏上 = A = -Y(+Y 是屁股 P) n[2] = 1.0; // 从头(S)往脚(I)看 break; case ViewAxis::Sagittal: // 从患者右看:上S下I 左P右A u[1] = -1.0; // 右 = A = -Y v[2] = 1.0; // 上 = S = +Z n[0] = -1.0; // 患者右 = -X,从右侧看 break; case ViewAxis::Coronal: // 面朝患者、约定俗成:上S下I 左L右R(图像水平翻转) u[0] = -1.0; // 屏右 = R = -X;屏左 = L = +X v[2] = 1.0; // 上 = S = +Z n[1] = -1.0; // 从 A(-Y) 看向 P break; } } void SliceViewWidget::inPlaneWorldAxes(int& uAxis, int& vAxis) const { switch (m_axis) { case ViewAxis::Axial: uAxis = 0; vAxis = 1; break; case ViewAxis::Sagittal: uAxis = 1; vAxis = 2; break; case ViewAxis::Coronal: uAxis = 0; vAxis = 2; break; } } void SliceViewWidget::updateOrientationLabels() { if (!m_labelTop) return; const int m = 6; m_labelTop->move((width() - m_labelTop->sizeHint().width()) / 2, m); m_labelBottom->move((width() - m_labelBottom->sizeHint().width()) / 2, height() - m_labelBottom->sizeHint().height() - m); m_labelLeft->move(m, (height() - m_labelLeft->sizeHint().height()) / 2); m_labelRight->move(width() - m_labelRight->sizeHint().width() - m, (height() - m_labelRight->sizeHint().height()) / 2); m_labelTop->raise(); m_labelBottom->raise(); m_labelLeft->raise(); m_labelRight->raise(); m_pickLabel->raise(); } void SliceViewWidget::setBackground(DoseImage::Pointer bg) { m_bg = std::move(bg); rebuildPipeline(); } void SliceViewWidget::setFront(DoseImage::Pointer fg) { m_fg = std::move(fg); if (!m_bg) { // 无背景时不必单独建 front return; } // 轻量更新:不拆整管线 if (m_fg) { m_renderer->RemoveActor(m_actorFg); m_renderer->AddActor(m_actorFg); m_fgSlice = samplePhysicalSlice(m_fg); rebuildColorMaps(); m_mapFg->SetInputData(m_fgSlice); m_mapFg->SetLookupTable(m_lutFg); m_mapFg->SetOutputFormatToRGB(); m_mapFg->Update(); if (auto* mapper = m_actorFg->GetMapper()) mapper->SetInputConnection(m_mapFg->GetOutputPort()); m_actorFg->SetOpacity(m_frontOpacity); m_actorFg->InterpolateOn(); } else { m_renderer->RemoveActor(m_actorFg); m_fgSlice = nullptr; } m_vtkWidget->GetRenderWindow()->Render(); } void SliceViewWidget::setOpacity(double frontOpacity) { m_frontOpacity = std::max(0.0, std::min(1.0, frontOpacity)); if (m_fg) { m_actorFg->SetOpacity(m_frontOpacity); m_actorBg->SetOpacity(1.0); } else { m_actorBg->SetOpacity(1.0); } m_vtkWidget->GetRenderWindow()->Render(); } Vec3d SliceViewWidget::sliceOrigin() const { // m_viewCenter = 体中心(仅加载/复位时设置),平移只动相机、绝不改它。 // 否则:pan 把 viewCenter 写成 ori+fp,滚轮重采样后再叠加相机 fp → 双倍偏移,放大后看不见图。 double u[3], v[3], n[3]; viewDirections(u, v, n); const double dx = m_cursor.x - m_viewCenter.x; const double dy = m_cursor.y - m_viewCenter.y; const double dz = m_cursor.z - m_viewCenter.z; const double dn = dx * n[0] + dy * n[1] + dz * n[2]; return {m_viewCenter.x + dn * n[0], m_viewCenter.y + dn * n[1], m_viewCenter.z + dn * n[2]}; } void SliceViewWidget::physicalStepUV(double& su, double& sv) const { if (!m_bg) { su = sv = 1.0; return; } // 用 ITK spacing 在 U/V 上的物理步长(取主导分量) const auto sp = m_bg->spacing(); double u[3], v[3], n[3]; viewDirections(u, v, n); su = std::max(1e-3, std::abs(u[0]) * std::abs(sp[0]) + std::abs(u[1]) * std::abs(sp[1]) + std::abs(u[2]) * std::abs(sp[2])); sv = std::max(1e-3, std::abs(v[0]) * std::abs(sp[0]) + std::abs(v[1]) * std::abs(sp[1]) + std::abs(v[2]) * std::abs(sp[2])); } void SliceViewWidget::sliceSizeUV(double& halfU, double& halfV) const { halfU = halfV = 50.0; if (!m_bg) return; double bmin[3], bmax[3]; m_bg->worldBounds(bmin, bmax); double u[3], v[3], n[3]; viewDirections(u, v, n); const Vec3d ori = sliceOrigin(); halfU = halfV = 0.0; for (int iz = 0; iz < 2; ++iz) for (int iy = 0; iy < 2; ++iy) for (int ix = 0; ix < 2; ++ix) { const double px = (ix ? bmax[0] : bmin[0]) - ori.x; const double py = (iy ? bmax[1] : bmin[1]) - ori.y; const double pz = (iz ? bmax[2] : bmin[2]) - ori.z; halfU = std::max(halfU, std::abs(px * u[0] + py * u[1] + pz * u[2])); halfV = std::max(halfV, std::abs(px * v[0] + py * v[1] + pz * v[2])); } halfU = std::max(halfU, 1.0); halfV = std::max(halfV, 1.0); } vtkSmartPointer SliceViewWidget::samplePhysicalSlice( const DoseImage::Pointer& dose) const { if (!dose) return nullptr; double u[3], v[3], n[3]; viewDirections(u, v, n); const Vec3d ori = sliceOrigin(); double su = m_su, sv = m_sv; // 始终按体数据全范围采样;缩放只改相机,绝不缩小采样半宽(否则滚轮切层会坏) double halfU = 0.0, halfV = 0.0; sliceSizeUV(halfU, halfV); int nu = static_cast(std::ceil(2.0 * halfU / su)) + 1; int nv = static_cast(std::ceil(2.0 * halfV / sv)) + 1; nu = std::max(nu, 8); nv = std::max(nv, 8); // 限制分辨率,避免过大 const int kMax = 1024; if (nu > kMax) { su *= static_cast(nu) / kMax; nu = kMax; } if (nv > kMax) { sv *= static_cast(nv) / kMax; nv = kMax; } auto img = vtkSmartPointer::New(); img->SetDimensions(nu, nv, 1); img->SetSpacing(su, sv, 1.0); // 2D 局部坐标:中心在 (0,0),便于相机固定看 XY img->SetOrigin(-0.5 * (nu - 1) * su, -0.5 * (nv - 1) * sv, 0.0); img->AllocateScalars(VTK_FLOAT, 1); auto* ptr = static_cast(img->GetScalarPointer()); const double x0 = -0.5 * (nu - 1) * su; const double y0 = -0.5 * (nv - 1) * sv; for (int j = 0; j < nv; ++j) { const double tv = y0 + j * sv; for (int i = 0; i < nu; ++i) { const double tu = x0 + i * su; const double wx = ori.x + tu * u[0] + tv * v[0]; const double wy = ori.y + tu * u[1] + tv * v[1]; const double wz = ori.z + tu * u[2] + tv * v[2]; ptr[j * nu + i] = dose->sampleWorld(wx, wy, wz); } } return img; } void SliceViewWidget::setCursorWorld(const Vec3d& world) { if (!m_bg) { m_cursor = world; m_hasCursor = true; return; } const Vec3d oldOri = sliceOrigin(); m_cursor = world; m_hasCursor = true; const Vec3d newOri = sliceOrigin(); const double d2 = (oldOri.x - newOri.x) * (oldOri.x - newOri.x) + (oldOri.y - newOri.y) * (oldOri.y - newOri.y) + (oldOri.z - newOri.z) * (oldOri.z - newOri.z); if (d2 > 1e-8) updateSlice(true); updateCrosshair(); m_vtkWidget->GetRenderWindow()->Render(); } void SliceViewWidget::resetViewToVolumeCenter() { if (!m_bg) return; double bmin[3], bmax[3]; m_bg->worldBounds(bmin, bmax); m_cursor = {(bmin[0] + bmax[0]) * 0.5, (bmin[1] + bmax[1]) * 0.5, (bmin[2] + bmax[2]) * 0.5}; m_viewCenter = m_cursor; m_hasCursor = true; physicalStepUV(m_su, m_sv); sliceSizeUV(m_halfU, m_halfV); updateSlice(false); setupCamera(true); updateCrosshair(); m_vtkWidget->GetRenderWindow()->Render(); emit viewExtentChanged(); } void SliceViewWidget::setWindowLevel(double window, double level) { m_window = window; m_level = level; rebuildColorMaps(); refreshMappedImages(); } void SliceViewWidget::resetWindowLevelToData() { if (!m_bg) return; m_window = static_cast(m_bg->maxValue() - m_bg->minValue()); m_level = static_cast((m_bg->maxValue() + m_bg->minValue()) * 0.5); if (m_window < 1e-6) m_window = 1.0; rebuildColorMaps(); emit windowLevelChanged(m_window, m_level); } bool SliceViewWidget::visibleRangeForWorldAxis(int worldAxis, double& rmin, double& rmax) const { if (!m_renderer || !m_vtkWidget || !m_cameraReady) return false; int uAxis = 0, vAxis = 1; inPlaneWorldAxes(uAxis, vAxis); if (worldAxis != uAxis && worldAxis != vAxis) return false; auto* cam = m_renderer->GetActiveCamera(); if (!cam) return false; const double halfH = cam->GetParallelScale(); const double aspect = static_cast(std::max(1, m_vtkWidget->width())) / static_cast(std::max(1, m_vtkWidget->height())); const double halfW = halfH * aspect; double fp[3]; cam->GetFocalPoint(fp); double u[3], v[3], n[3]; viewDirections(u, v, n); const Vec3d ori = sliceOrigin(); // 可视中心 = 切片原点 + 相机焦平面内偏移(与 pan 一致) const double cx = ori.x + fp[0] * u[0] + fp[1] * v[0]; const double cy = ori.y + fp[0] * u[1] + fp[1] * v[1]; const double cz = ori.z + fp[0] * u[2] + fp[1] * v[2]; const double center[3] = {cx, cy, cz}; const double half = (worldAxis == uAxis) ? halfW : halfH; rmin = center[worldAxis] - half; rmax = center[worldAxis] + half; return true; } bool SliceViewWidget::displayToWorld(int displayX, int displayY, Vec3d& out) const { if (!m_renderer || !m_cameraReady) return false; auto* cam = m_renderer->GetActiveCamera(); if (!cam) return false; const int w = std::max(1, m_vtkWidget->width()); const int h = std::max(1, m_vtkWidget->height()); const double halfH = cam->GetParallelScale(); const double halfW = halfH * static_cast(w) / static_cast(h); double fp[3]; cam->GetFocalPoint(fp); // 切片画在局部 XY:fp 约在 (0,0,0),U→X,V→Y const double du = (displayX - w * 0.5) / (w * 0.5) * halfW + fp[0]; const double dv = (h * 0.5 - displayY) / (h * 0.5) * halfH + fp[1]; double u[3], v[3], n[3]; viewDirections(u, v, n); const Vec3d ori = sliceOrigin(); out.x = ori.x + du * u[0] + dv * v[0]; out.y = ori.y + du * u[1] + dv * v[1]; out.z = ori.z + du * u[2] + dv * v[2]; return true; } void SliceViewWidget::rebuildPipeline() { m_renderer->RemoveActor(m_actorBg); m_renderer->RemoveActor(m_actorFg); m_bgSlice = nullptr; m_fgSlice = nullptr; m_cameraReady = false; if (!m_bg) { if (m_vtkWidget && m_vtkWidget->GetRenderWindow()) m_vtkWidget->GetRenderWindow()->Render(); return; } double bmin[3], bmax[3]; m_bg->worldBounds(bmin, bmax); m_cursor = {(bmin[0] + bmax[0]) * 0.5, (bmin[1] + bmax[1]) * 0.5, (bmin[2] + bmax[2]) * 0.5}; m_viewCenter = m_cursor; m_hasCursor = true; physicalStepUV(m_su, m_sv); sliceSizeUV(m_halfU, m_halfV); m_renderer->AddActor(m_actorBg); if (m_fg) m_renderer->AddActor(m_actorFg); resetWindowLevelToData(); updateSlice(false); setupCamera(true); updateCrosshair(); m_vtkWidget->GetRenderWindow()->Render(); emit viewExtentChanged(); } void SliceViewWidget::updateSlice(bool /*moveCamera*/) { if (!m_bg) return; // 保留相机(缩放/平移),切层绝不能 reset 相机 double savedScale = 1.0; double savedFp[3] = {0, 0, 0}; double savedPos[3] = {0, 0, 1}; double savedUp[3] = {0, 1, 0}; auto* cam = m_renderer ? m_renderer->GetActiveCamera() : nullptr; if (cam && m_cameraReady) { savedScale = cam->GetParallelScale(); cam->GetFocalPoint(savedFp); cam->GetPosition(savedPos); cam->GetViewUp(savedUp); } physicalStepUV(m_su, m_sv); // 采样范围始终用体全图,与相机缩放解耦 sliceSizeUV(m_halfU, m_halfV); m_bgSlice = samplePhysicalSlice(m_bg); if (!m_bgSlice) return; rebuildColorMaps(); m_mapBg->SetInputData(m_bgSlice); m_mapBg->SetLookupTable(m_lutBg); m_mapBg->SetOutputFormatToRGB(); m_mapBg->Update(); if (auto* mapper = m_actorBg->GetMapper()) { mapper->SetInputConnection(m_mapBg->GetOutputPort()); } m_actorBg->InterpolateOn(); m_actorBg->Update(); if (m_fg) { m_fgSlice = samplePhysicalSlice(m_fg); if (m_fgSlice) { m_mapFg->SetInputData(m_fgSlice); m_mapFg->SetLookupTable(m_lutFg); m_mapFg->SetOutputFormatToRGB(); m_mapFg->Update(); if (auto* mapper = m_actorFg->GetMapper()) mapper->SetInputConnection(m_mapFg->GetOutputPort()); m_actorFg->SetOpacity(m_frontOpacity); m_actorFg->InterpolateOn(); m_actorFg->Update(); } } if (cam && m_cameraReady) { cam->SetParallelProjection(true); cam->SetParallelScale(savedScale); cam->SetFocalPoint(savedFp); cam->SetPosition(savedPos); cam->SetViewUp(savedUp); m_renderer->ResetCameraClippingRange(); } } void SliceViewWidget::setupCamera(bool resetScale) { if (!m_renderer) return; auto* cam = m_renderer->GetActiveCamera(); if (!cam) return; // 切片在局部 XY 平面,相机沿 +Z 看,ViewUp=+Y cam->SetParallelProjection(true); cam->SetFocalPoint(0.0, 0.0, 0.0); cam->SetPosition(0.0, 0.0, 1.0); cam->SetViewUp(0.0, 1.0, 0.0); if (resetScale) { const double half = std::max(m_halfU, m_halfV); cam->SetParallelScale(std::max(1.0, half * 0.55)); } m_renderer->ResetCameraClippingRange(); m_cameraReady = true; } void SliceViewWidget::updateCrosshair() { if (!m_renderer || !m_vtkWidget || !m_cameraReady) return; auto* cam = m_renderer->GetActiveCamera(); if (!cam) return; double u[3], v[3], n[3]; viewDirections(u, v, n); const Vec3d ori = sliceOrigin(); const double dx = m_cursor.x - ori.x; const double dy = m_cursor.y - ori.y; const double dz = m_cursor.z - ori.z; const double tu = dx * u[0] + dy * u[1] + dz * u[2]; const double tv = dx * v[0] + dy * v[1] + dz * v[2]; m_renderer->SetWorldPoint(tu, tv, 0.0, 1.0); m_renderer->WorldToDisplay(); double disp[3]; m_renderer->GetDisplayPoint(disp); if (!std::isfinite(disp[0]) || !std::isfinite(disp[1])) return; const int w = std::max(1, m_vtkWidget->width()); const int h = std::max(1, m_vtkWidget->height()); const double cx = disp[0]; const double cy = disp[1]; auto setLine = [](vtkActor2D* actor, double x0, double y0, double x1, double y1, const double rgb[3]) { auto* mapper = vtkPolyDataMapper2D::SafeDownCast(actor->GetMapper()); auto* poly = vtkPolyData::SafeDownCast(mapper->GetInput()); auto* pts = poly->GetPoints(); pts->SetPoint(0, x0, y0, 0); pts->SetPoint(1, x1, y1, 0); pts->Modified(); poly->Modified(); actor->GetProperty()->SetColor(rgb[0], rgb[1], rgb[2]); }; double rgbU[3], rgbV[3]; axisColorRgb(colorForLineDirection(u), rgbU); axisColorRgb(colorForLineDirection(v), rgbV); setLine(m_crossH, 0, cy, w, cy, rgbU); setLine(m_crossV, cx, 0, cx, h, rgbV); } void SliceViewWidget::updatePickOverlay(const QPoint& pos) { Vec3d world; if (!displayToWorld(pos.x(), pos.y(), world) || !m_bg) { m_pickLabel->hide(); return; } const float bVal = m_bg->sampleWorld(world.x, world.y, world.z); QString text; if (m_fg) { const float fVal = m_fg->sampleWorld(world.x, world.y, world.z); text = QStringLiteral("F: %1\nB: %2").arg(fVal, 0, 'f', 3).arg(bVal, 0, 'f', 3); } else { text = QStringLiteral("F: -\nB: %1").arg(bVal, 0, 'f', 3); } m_pickLabel->setText(text); m_pickLabel->adjustSize(); m_pickLabel->move(std::max(0, width() - m_pickLabel->width() - 8), std::max(0, height() - m_pickLabel->height() - 8)); m_pickLabel->show(); m_pickLabel->raise(); } void SliceViewWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); updateOrientationLabels(); updateCrosshair(); emit viewExtentChanged(); } bool SliceViewWidget::eventFilter(QObject* obj, QEvent* event) { if (obj != m_vtkWidget) return QWidget::eventFilter(obj, event); switch (event->type()) { case QEvent::MouseButtonPress: { auto* e = static_cast(event); m_lastPos = e->pos(); if (e->button() == Qt::LeftButton) { m_leftDown = true; return true; } if (e->button() == Qt::RightButton) { m_rightDown = true; return true; } if (e->button() == Qt::MiddleButton) { m_middleDown = true; return true; } break; } case QEvent::MouseButtonRelease: { auto* e = static_cast(event); if (e->button() == Qt::LeftButton) m_leftDown = false; else if (e->button() == Qt::RightButton) m_rightDown = false; else if (e->button() == Qt::MiddleButton) m_middleDown = false; return true; } case QEvent::MouseMove: { auto* e = static_cast(event); const int dx = e->x() - m_lastPos.x(); const int dy = e->y() - m_lastPos.y(); m_lastPos = e->pos(); updatePickOverlay(e->pos()); // Ctrl + 移动:在鼠标位置重切另外两个面(更新交点) if ((e->modifiers() & Qt::ControlModifier) && !m_leftDown && !m_rightDown && !m_middleDown) { Vec3d world; if (displayToWorld(e->x(), e->y(), world)) emit cursorChanged(world); return true; } if (m_leftDown) { applyMouseWindowLevel(dx, dy); return true; } if (m_rightDown) { applyPan(dx, dy); return true; } if (m_middleDown) { applyZoomByDrag(dy); return true; } return true; } case QEvent::Leave: m_pickLabel->hide(); break; case QEvent::Wheel: { auto* e = static_cast(event); const int steps = (e->angleDelta().y() > 0) ? 1 : (e->angleDelta().y() < 0 ? -1 : 0); if (steps != 0) applySliceStep(steps); return true; } default: break; } return QWidget::eventFilter(obj, event); } void SliceViewWidget::applyMouseWindowLevel(int dx, int dy) { m_window = std::max(1e-3, m_window + dx * (m_window * 0.005 + 0.01)); m_level = m_level - dy * (m_window * 0.005 + 0.01); setWindowLevel(m_window, m_level); emit windowLevelChanged(m_window, m_level); } void SliceViewWidget::applyPan(int dx, int dy) { auto* cam = m_renderer->GetActiveCamera(); if (!cam) return; cam->SetParallelProjection(true); double fp[3], pos[3]; cam->GetFocalPoint(fp); cam->GetPosition(pos); const double scale = cam->GetParallelScale() * 0.002; const double dU = -dx * scale; const double dV = dy * scale; fp[0] += dU; fp[1] += dV; pos[0] += dU; pos[1] += dV; cam->SetFocalPoint(fp); cam->SetPosition(pos); double u[3], v[3], n[3]; viewDirections(u, v, n); const double wdx = dU * u[0] + dV * v[0]; const double wdy = dU * u[1] + dV * v[1]; const double wdz = dU * u[2] + dV * v[2]; updateCrosshair(); m_vtkWidget->GetRenderWindow()->Render(); emit panWorldDelta(wdx, wdy, wdz); emit viewExtentChanged(); } void SliceViewWidget::applyZoomByDrag(int dy) { auto* cam = m_renderer->GetActiveCamera(); if (!cam) return; double scale = cam->GetParallelScale(); scale *= std::max(0.5, std::min(1.5, 1.0 + dy * 0.01)); scale = std::max(0.1, scale); cam->SetParallelScale(scale); updateCrosshair(); m_vtkWidget->GetRenderWindow()->Render(); emit zoomScaleChanged(scale); emit viewExtentChanged(); } double SliceViewWidget::parallelScale() const { if (!m_renderer || !m_cameraReady) return 1.0; auto* cam = m_renderer->GetActiveCamera(); return cam ? cam->GetParallelScale() : 1.0; } void SliceViewWidget::setParallelScaleShared(double scale) { if (!m_renderer || !m_cameraReady) return; auto* cam = m_renderer->GetActiveCamera(); if (!cam) return; cam->SetParallelScale(std::max(0.1, scale)); m_renderer->ResetCameraClippingRange(); updateCrosshair(); if (m_vtkWidget && m_vtkWidget->GetRenderWindow()) m_vtkWidget->GetRenderWindow()->Render(); } void SliceViewWidget::applyWorldPanShared(double dx, double dy, double dz) { if (!m_renderer || !m_cameraReady) return; auto* cam = m_renderer->GetActiveCamera(); if (!cam) return; double u[3], v[3], n[3]; viewDirections(u, v, n); const double dU = dx * u[0] + dy * u[1] + dz * u[2]; const double dV = dx * v[0] + dy * v[1] + dz * v[2]; if (std::abs(dU) < 1e-12 && std::abs(dV) < 1e-12) return; cam->SetParallelProjection(true); double fp[3], pos[3]; cam->GetFocalPoint(fp); cam->GetPosition(pos); fp[0] += dU; fp[1] += dV; pos[0] += dU; pos[1] += dV; cam->SetFocalPoint(fp); cam->SetPosition(pos); updateCrosshair(); if (m_vtkWidget && m_vtkWidget->GetRenderWindow()) m_vtkWidget->GetRenderWindow()->Render(); } void SliceViewWidget::applySliceStep(int steps) { if (!m_bg || steps == 0) return; const auto sp = m_bg->spacing(); double u[3], v[3], n[3]; viewDirections(u, v, n); double stepLen = std::abs(sp[2]); if (m_axis == ViewAxis::Sagittal) stepLen = std::abs(sp[0]); else if (m_axis == ViewAxis::Coronal) stepLen = std::abs(sp[1]); Vec3d c = m_cursor; c.x += n[0] * steps * stepLen; c.y += n[1] * steps * stepLen; c.z += n[2] * steps * stepLen; double bmin[3], bmax[3]; m_bg->worldBounds(bmin, bmax); c.x = std::max(bmin[0], std::min(bmax[0], c.x)); c.y = std::max(bmin[1], std::min(bmax[1], c.y)); c.z = std::max(bmin[2], std::min(bmax[2], c.z)); emit cursorChanged(c); }