#include "nmVTKScalarBarLayer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { /** * @brief 只公开 VTK 色标的按需布局入口,供独立 Overlay 在 Render 前读取新几何。 */ class vtkNmScalarBarActor : public vtkScalarBarActor { public: static vtkNmScalarBarActor* New(); vtkTypeMacro(vtkNmScalarBarActor, vtkScalarBarActor); /** @brief 立即同步内部色带、刻度和文字布局,不触发 Renderer 绘制。 */ int refreshLayout(vtkViewport* pViewport) { return pViewport != nullptr ? RebuildLayoutIfNeeded(pViewport) : 0; } protected: vtkNmScalarBarActor() { } virtual ~vtkNmScalarBarActor() { } private: vtkNmScalarBarActor(const vtkNmScalarBarActor&); void operator=(const vtkNmScalarBarActor&); }; vtkStandardNewMacro(vtkNmScalarBarActor); const int SCALAR_BAR_LEFT_MARGIN_PIXELS = 20; const int SCALAR_BAR_TOTAL_WIDTH_PIXELS = 108; // VTK 内部留白会压缩色带,目标宽度设为 34px 后可见宽度约为 30px。 const int SCALAR_BAR_COLOR_WIDTH_PIXELS = 34; const int SCALAR_BAR_VISIBLE_COLOR_WIDTH_PIXELS = 30; const int SCALAR_BAR_TITLE_FONT_SIZE = 18; const int SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS = 24; const int SCALAR_BAR_LABEL_FONT_SIZE = 15; const int SCALAR_BAR_SMALL_VIEWPORT_HEIGHT = 450; const int SCALAR_BAR_MEDIUM_VIEWPORT_HEIGHT = 650; const int SCALAR_BAR_LABEL_SPACING_PIXELS = 80; const int SCALAR_BAR_MIN_LABEL_COUNT = 6; const int SCALAR_BAR_MAX_LABEL_COUNT = 9; const int SCALAR_BAR_TITLE_OFFSET_Y_PIXELS = 28; const int SCALAR_BAR_SWATCH_WIDTH_PIXELS = 16; const int SCALAR_BAR_SWATCH_HEIGHT_PIXELS = 12; const int SCALAR_BAR_SWATCH_GAP_PIXELS = 4; const int SCALAR_BAR_TEXT_GAP_PIXELS = 6; const int SCALAR_BAR_LABEL_GAP_PIXELS = 8; const int SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS = 4; const double SCALAR_BAR_NORMAL_BOTTOM_RATIO = 0.22; const double SCALAR_BAR_NORMAL_HEIGHT_RATIO = 0.67; const double SCALAR_BAR_SMALL_BOTTOM_RATIO = 0.22; const double SCALAR_BAR_SMALL_HEIGHT_RATIO = 0.62; // ScalarBarActor 和 LookupTable 均只能在 Qt GUI 线程中修改。 bool isScalarBarGuiThread() { QCoreApplication* pApplication = QCoreApplication::instance(); return pApplication == nullptr || pApplication->thread() == QThread::currentThread(); } // 独立色块使用归一化视口坐标,避免依赖仅支持 IndexedLookup 的 ScalarBar Annotation。 vtkSmartPointer createRangeSwatchActor(vtkPoints* pPoints) { vtkSmartPointer pPolygons = vtkSmartPointer::New(); pPolygons->InsertNextCell(4); pPolygons->InsertCellPoint(0); pPolygons->InsertCellPoint(1); pPolygons->InsertCellPoint(2); pPolygons->InsertCellPoint(3); vtkSmartPointer pData = vtkSmartPointer::New(); pData->SetPoints(pPoints); pData->SetPolys(pPolygons); vtkSmartPointer pViewportCoordinate = vtkSmartPointer::New(); pViewportCoordinate->SetCoordinateSystemToNormalizedViewport(); vtkSmartPointer pMapper = vtkSmartPointer::New(); pMapper->SetInputData(pData); pMapper->SetTransformCoordinate(pViewportCoordinate); pMapper->ScalarVisibilityOff(); vtkSmartPointer pActor = vtkSmartPointer::New(); pActor->SetMapper(pMapper); pActor->GetProperty()->SetDisplayLocationToForeground(); pActor->PickableOff(); pActor->SetVisibility(0); return pActor; } // 独立文本共用字号和颜色;单值端点可选择与普通刻度一致的强调样式。 void configureOverlayTextActor(vtkTextActor* pActor, bool bUseTickStyle) { pActor->SetInput(""); pActor->SetTextScaleModeToNone(); pActor->GetTextProperty()->SetFontFamilyToArial(); pActor->GetTextProperty()->SetFontSize(SCALAR_BAR_LABEL_FONT_SIZE); pActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0); if(bUseTickStyle) { pActor->GetTextProperty()->BoldOn(); pActor->GetTextProperty()->ItalicOn(); pActor->GetTextProperty()->ShadowOn(); pActor->GetTextProperty()->SetShadowOffset(1, -1); } else { pActor->GetTextProperty()->BoldOff(); pActor->GetTextProperty()->ItalicOff(); pActor->GetTextProperty()->ShadowOff(); } pActor->GetTextProperty()->SetJustificationToLeft(); pActor->GetTextProperty()->SetVerticalJustificationToCentered(); pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); pActor->GetProperty()->SetDisplayLocationToForeground(); pActor->PickableOff(); pActor->SetVisibility(0); } // 独立标题不受 vtkScalarBarActor 内部居中布局影响,始终保持左对齐。 void configureTitleActor(vtkTextActor* pActor) { pActor->SetInput(""); pActor->SetTextScaleModeToNone(); pActor->GetTextProperty()->SetFontFamilyToArial(); pActor->GetTextProperty()->SetFontSize(SCALAR_BAR_TITLE_FONT_SIZE); pActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0); pActor->GetTextProperty()->BoldOff(); pActor->GetTextProperty()->ItalicOff(); pActor->GetTextProperty()->ShadowOn(); pActor->GetTextProperty()->SetShadowOffset(1, -1); pActor->GetTextProperty()->SetJustificationToLeft(); pActor->GetTextProperty()->SetVerticalJustificationToBottom(); pActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); pActor->GetProperty()->SetDisplayLocationToForeground(); pActor->PickableOff(); pActor->SetVisibility(0); } // 优先读取 VTK 按当前 DPI 计算的真实文字高度,尚未挂载时使用稳定回退值。 double getOverlayTextHeightPixels(vtkTextActor* pActor, vtkRenderer* pRenderer, double dFallbackHeight) { if(pActor == nullptr || pRenderer == nullptr) { return dFallbackHeight; } double dSize[2] = { 0.0, 0.0 }; pActor->GetSize(pRenderer, dSize); return vtkMath::IsFinite(dSize[1]) && dSize[1] > 0.0 ? dSize[1] : dFallbackHeight; } // 横向避让仅在极小视口空间不足时使用,正常布局仍保持色块位于色带上方。 double getOverlayTextWidthPixels(vtkTextActor* pActor, vtkRenderer* pRenderer, double dFallbackWidth) { if(pActor == nullptr || pRenderer == nullptr) { return dFallbackWidth; } double dSize[2] = { 0.0, 0.0 }; pActor->GetSize(pRenderer, dSize); return vtkMath::IsFinite(dSize[0]) && dSize[0] > 0.0 ? dSize[0] : dFallbackWidth; } // 更新矩形四角并显式标记 Points 已修改,Mapper 会在下一次 Render 使用新位置。 void updateRectangle(vtkPoints* pPoints, double dX, double dY, double dWidth, double dHeight) { pPoints->SetPoint(0, dX, dY, 0.0); pPoints->SetPoint(1, dX + dWidth, dY, 0.0); pPoints->SetPoint(2, dX + dWidth, dY + dHeight, 0.0); pPoints->SetPoint(3, dX, dY + dHeight, 0.0); pPoints->Modified(); } } nmVTKScalarBarLayer::nmVTKScalarBarLayer(const QString& sLayerId) : nmVTKRenderLayer(sLayerId), m_pLookupTable(nullptr), m_pRootAssembly(vtkSmartPointer::New()), m_pActor(vtkSmartPointer::New()), m_pTitleActor(vtkSmartPointer::New()), m_pConstantMinimumValueActor(vtkSmartPointer::New()), m_pConstantMaximumValueActor(vtkSmartPointer::New()), m_pBelowRangeSwatchPoints(vtkSmartPointer::New()), m_pBelowRangeSwatchActor(nullptr), m_pBelowRangeTextActor(vtkSmartPointer::New()), m_pAboveRangeSwatchPoints(vtkSmartPointer::New()), m_pAboveRangeSwatchActor(nullptr), m_pAboveRangeTextActor(vtkSmartPointer::New()), m_sLabelFormat(QString::fromLatin1("%.6g")), m_dPositionX(0.0), m_dPositionY(0.0), m_dWidth(0.1), m_dHeight(0.8), m_nViewportWidth(0), m_nViewportHeight(0) { Q_ASSERT(isScalarBarGuiThread()); // vtkScalarBarActor 是二维 Prop,仍通过图层基类统一完成挂载和显隐。 m_pActor->SetNumberOfLabels(5); m_pActor->SetLabelFormat("%.6g"); m_pActor->SetTitle(""); m_pActor->SetOrientationToVertical(); m_pActor->SetTextPositionToSucceedScalarBar(); m_pActor->SetTextPad(3); m_pActor->UnconstrainedFontSizeOn(); m_pActor->GetLabelTextProperty()->SetFontFamilyToArial(); m_pActor->GetLabelTextProperty()->SetFontSize( SCALAR_BAR_LABEL_FONT_SIZE); m_pActor->GetLabelTextProperty()->SetColor(1.0, 1.0, 1.0); m_pActor->GetLabelTextProperty()->BoldOn(); m_pActor->GetLabelTextProperty()->ItalicOn(); m_pActor->GetLabelTextProperty()->ShadowOn(); m_pActor->GetLabelTextProperty()->SetShadowOffset(1, -1); m_pActor->SetMaximumWidthInPixels(SCALAR_BAR_TOTAL_WIDTH_PIXELS); m_pActor->SetBarRatio( static_cast(SCALAR_BAR_COLOR_WIDTH_PIXELS) / static_cast(SCALAR_BAR_TOTAL_WIDTH_PIXELS)); m_pActor->GetProperty()->SetDisplayLocationToForeground(); m_pActor->PickableOff(); m_pActor->DrawAnnotationsOff(); m_pActor->DrawBelowRangeSwatchOff(); m_pActor->DrawAboveRangeSwatchOff(); // 这些二维装饰必须独立显示,不能为网格共享 LUT 开启 IndexedLookup。 m_pBelowRangeSwatchPoints->SetNumberOfPoints(4); m_pAboveRangeSwatchPoints->SetNumberOfPoints(4); m_pBelowRangeSwatchActor = createRangeSwatchActor(m_pBelowRangeSwatchPoints); m_pAboveRangeSwatchActor = createRangeSwatchActor(m_pAboveRangeSwatchPoints); configureTitleActor(m_pTitleActor); configureOverlayTextActor(m_pConstantMinimumValueActor, true); configureOverlayTextActor(m_pConstantMaximumValueActor, true); configureOverlayTextActor(m_pBelowRangeTextActor, false); configureOverlayTextActor(m_pAboveRangeTextActor, false); updateOverlayLayout(); // 根 Assembly 只在构造时注册,运行期仅更新子 Actor 状态。 m_pRootAssembly->AddPart(m_pActor); m_pRootAssembly->AddPart(m_pTitleActor); m_pRootAssembly->AddPart(m_pConstantMinimumValueActor); m_pRootAssembly->AddPart(m_pConstantMaximumValueActor); m_pRootAssembly->AddPart(m_pBelowRangeSwatchActor); m_pRootAssembly->AddPart(m_pBelowRangeTextActor); m_pRootAssembly->AddPart(m_pAboveRangeSwatchActor); m_pRootAssembly->AddPart(m_pAboveRangeTextActor); setRenderProp(m_pRootAssembly); } nmVTKScalarBarLayer::~nmVTKScalarBarLayer() { Q_ASSERT(isScalarBarGuiThread()); // 先移除二维 Prop,再释放对外部 LookupTable 的共享引用。 detach(); m_pLookupTable = nullptr; m_pAboveRangeTextActor = nullptr; m_pAboveRangeSwatchActor = nullptr; m_pAboveRangeSwatchPoints = nullptr; m_pBelowRangeTextActor = nullptr; m_pBelowRangeSwatchActor = nullptr; m_pBelowRangeSwatchPoints = nullptr; m_pConstantMaximumValueActor = nullptr; m_pConstantMinimumValueActor = nullptr; m_pTitleActor = nullptr; m_pActor = nullptr; m_pRootAssembly = nullptr; } void nmVTKScalarBarLayer::setLookupTable(vtkLookupTable* pLookupTable) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread()) { return; } // 智能指针共享 LookupTable,保证外部释放原指针后色标仍可安全显示。 m_pLookupTable = pLookupTable; m_pActor->SetLookupTable(pLookupTable); updateRangeSwatchColors(); updateOverlayLayout(); } vtkLookupTable* nmVTKScalarBarLayer::getLookupTable() const { return m_pLookupTable; } void nmVTKScalarBarLayer::setTitle(const QString& sTitle) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread()) { return; } m_sTitle = sTitle; // 内置标题永久留空,避免其按整个 ScalarBarActor 宽度自动居中。 m_pActor->SetTitle(""); const QByteArray aTitle = sTitle.toUtf8(); m_pTitleActor->SetInput(aTitle.constData()); m_pTitleActor->SetVisibility(sTitle.isEmpty() ? 0 : 1); // 标题宽度参与极小视口下 Above max 的横向避让。 updateOverlayLayout(); } QString nmVTKScalarBarLayer::getTitle() const { return m_sTitle; } bool nmVTKScalarBarLayer::setLabelFormat(const QString& sFormat) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread() || sFormat.isEmpty()) { return false; } m_sLabelFormat = sFormat; const QByteArray aFormat = sFormat.toLatin1(); m_pActor->SetLabelFormat(aFormat.constData()); // 格式变化可能改变刻度宽度及 VTK 内部实际色带矩形。 updateOverlayLayout(); return true; } QString nmVTKScalarBarLayer::getLabelFormat() const { return m_sLabelFormat; } bool nmVTKScalarBarLayer::setNumberOfLabels(int nLabelCount) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread() || nLabelCount < 0 || nLabelCount > 64) { return false; } m_pActor->SetNumberOfLabels(nLabelCount); // 标签数量会影响 VTK 内部色带矩形,独立 Overlay 必须同步其最新位置。 updateOverlayLayout(); return true; } int nmVTKScalarBarLayer::getNumberOfLabels() const { return m_pActor != nullptr ? m_pActor->GetNumberOfLabels() : 0; } bool nmVTKScalarBarLayer::setViewportSize(int nWidth, int nHeight) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread() || nWidth <= 0 || nHeight <= 0) { return false; } m_nViewportWidth = nWidth; m_nViewportHeight = nHeight; const bool bSmallViewport = nHeight < SCALAR_BAR_SMALL_VIEWPORT_HEIGHT; const double dBottomRatio = bSmallViewport ? SCALAR_BAR_SMALL_BOTTOM_RATIO : SCALAR_BAR_NORMAL_BOTTOM_RATIO; const double dHeightRatio = bSmallViewport ? SCALAR_BAR_SMALL_HEIGHT_RATIO : SCALAR_BAR_NORMAL_HEIGHT_RATIO; // 小于目标宽度的极端视口会安全收窄;正常窗口保持固定像素宽度。 const int nLeftMargin = qMin( SCALAR_BAR_LEFT_MARGIN_PIXELS, qMax(0, nWidth - 1)); const int nLayoutWidth = qMin( SCALAR_BAR_TOTAL_WIDTH_PIXELS, qMax(1, nWidth - nLeftMargin)); const int nLayoutHeight = qMax(1, qRound(nHeight * dHeightRatio)); m_dPositionX = static_cast(nLeftMargin) / nWidth; m_dPositionY = dBottomRatio; m_dWidth = static_cast(nLayoutWidth) / nWidth; m_dHeight = static_cast(nLayoutHeight) / nHeight; m_pActor->SetPosition(m_dPositionX, m_dPositionY); m_pActor->SetWidth(m_dWidth); m_pActor->SetHeight(m_dHeight); m_pActor->SetMaximumWidthInPixels(nLayoutWidth); m_pActor->SetMaximumHeightInPixels(nLayoutHeight); double dBarRatio = static_cast(SCALAR_BAR_COLOR_WIDTH_PIXELS) / static_cast(nLayoutWidth); if(dBarRatio >= 1.0) { dBarRatio = 0.95; } m_pActor->SetBarRatio(dBarRatio); updateOverlayLayout(); return true; } int nmVTKScalarBarLayer::getResponsiveLabelCount() const { if(m_nViewportHeight <= 0) { return 5; } if(m_nViewportHeight < SCALAR_BAR_SMALL_VIEWPORT_HEIGHT) { return 4; } if(m_nViewportHeight <= SCALAR_BAR_MEDIUM_VIEWPORT_HEIGHT) { return 6; } const int nBarHeight = qMax( 1, qRound(m_dHeight * m_nViewportHeight)); return qBound( SCALAR_BAR_MIN_LABEL_COUNT, qRound(static_cast(nBarHeight) / SCALAR_BAR_LABEL_SPACING_PIXELS), SCALAR_BAR_MAX_LABEL_COUNT); } void nmVTKScalarBarLayer::setBelowRangeSwatch( bool bVisible, const QString& sText) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread()) { return; } m_sBelowRangeText = bVisible ? sText : QString(); const QByteArray aText = m_sBelowRangeText.toUtf8(); m_pBelowRangeTextActor->SetInput(aText.constData()); m_pBelowRangeSwatchActor->SetVisibility(bVisible ? 1 : 0); m_pBelowRangeTextActor->SetVisibility( bVisible && !m_sBelowRangeText.isEmpty() ? 1 : 0); updateOverlayLayout(); } void nmVTKScalarBarLayer::setAboveRangeSwatch( bool bVisible, const QString& sText) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread()) { return; } m_sAboveRangeText = bVisible ? sText : QString(); const QByteArray aText = m_sAboveRangeText.toUtf8(); m_pAboveRangeTextActor->SetInput(aText.constData()); m_pAboveRangeSwatchActor->SetVisibility(bVisible ? 1 : 0); m_pAboveRangeTextActor->SetVisibility( bVisible && !m_sAboveRangeText.isEmpty() ? 1 : 0); updateOverlayLayout(); } void nmVTKScalarBarLayer::setConstantValueLabel( bool bVisible, const QString& sText) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread()) { return; } m_sConstantValueText = bVisible ? sText : QString(); const QByteArray aText = m_sConstantValueText.toUtf8(); m_pConstantMinimumValueActor->SetInput(aText.constData()); m_pConstantMaximumValueActor->SetInput(aText.constData()); const int nVisibility = bVisible && !m_sConstantValueText.isEmpty() ? 1 : 0; m_pConstantMinimumValueActor->SetVisibility(nVisibility); m_pConstantMaximumValueActor->SetVisibility(nVisibility); updateOverlayLayout(); } bool nmVTKScalarBarLayer::setPosition(double dX, double dY) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread() || !vtkMath::IsFinite(dX) || !vtkMath::IsFinite(dY)) { return false; } // vtkScalarBarActor 默认使用归一化视口坐标,位置值由上层布局策略决定。 m_dPositionX = dX; m_dPositionY = dY; m_pActor->SetPosition(dX, dY); updateOverlayLayout(); return true; } bool nmVTKScalarBarLayer::setSize(double dWidth, double dHeight) { Q_ASSERT(isScalarBarGuiThread()); if(!isScalarBarGuiThread() || !vtkMath::IsFinite(dWidth) || !vtkMath::IsFinite(dHeight) || dWidth <= 0.0 || dHeight <= 0.0) { return false; } // 这里只拒绝非正尺寸,不替业务层限制色标是否超出视口。 m_dWidth = dWidth; m_dHeight = dHeight; m_pActor->SetWidth(dWidth); m_pActor->SetHeight(dHeight); updateOverlayLayout(); return true; } void nmVTKScalarBarLayer::updateOverlayLayout() { const bool bHasViewport = m_nViewportWidth > 0 && m_nViewportHeight > 0; const double dPixelX = bHasViewport ? 1.0 / m_nViewportWidth : 0.01; const double dPixelY = bHasViewport ? 1.0 / m_nViewportHeight : 0.01; double dColorX = m_dPositionX; double dColorY = m_dPositionY; double dColorWidth = bHasViewport ? SCALAR_BAR_VISIBLE_COLOR_WIDTH_PIXELS * dPixelX : m_dWidth * 0.33; double dColorHeight = m_dHeight; if(dColorWidth > m_dWidth) { dColorWidth = m_dWidth; } vtkRenderer* pRenderer = getAttachedRenderer(); vtkNmScalarBarActor* pLayoutActor = vtkNmScalarBarActor::SafeDownCast(m_pActor); if(bHasViewport && pRenderer != nullptr && m_pLookupTable != nullptr && pLayoutActor != nullptr && pLayoutActor->refreshLayout(pRenderer)) { // vtkScalarBarActor 的 Position/Size 是包含刻度留白的外框;端点和 // 其他装饰必须先刷新布局,再读取最终计算出的实际彩色色带矩形。 int nColorBarRect[4] = { 0, 0, 0, 0 }; m_pActor->GetScalarBarRect(nColorBarRect, pRenderer); double dLeft = nColorBarRect[0]; double dBottom = nColorBarRect[1]; double dRight = nColorBarRect[0] + nColorBarRect[2]; double dTop = nColorBarRect[1] + nColorBarRect[3]; pRenderer->ViewportToNormalizedViewport(dLeft, dBottom); pRenderer->ViewportToNormalizedViewport(dRight, dTop); if(vtkMath::IsFinite(dLeft) && vtkMath::IsFinite(dBottom) && vtkMath::IsFinite(dRight) && vtkMath::IsFinite(dTop) && dRight > dLeft && dTop > dBottom) { dColorX = dLeft; dColorY = dBottom; dColorWidth = dRight - dLeft; dColorHeight = dTop - dBottom; } } // 单值属性在色带上下端重复显示真实值,位置与普通端点刻度一致。 const double dLabelX = dColorX + dColorWidth + SCALAR_BAR_LABEL_GAP_PIXELS * dPixelX; m_pConstantMinimumValueActor->GetPositionCoordinate()->SetValue( dLabelX, dColorY, 0.0); m_pConstantMaximumValueActor->GetPositionCoordinate()->SetValue( dLabelX, dColorY + dColorHeight, 0.0); // 越界色块和文字也按实际像素布局,窗口缩放时不会横向漂移。 const double dSwatchWidth = bHasViewport ? SCALAR_BAR_SWATCH_WIDTH_PIXELS * dPixelX : qMin(m_dWidth * 0.25, 0.025); const double dSwatchHeight = bHasViewport ? SCALAR_BAR_SWATCH_HEIGHT_PIXELS * dPixelY : 0.018; const double dSwatchGap = bHasViewport ? SCALAR_BAR_SWATCH_GAP_PIXELS * dPixelY : 0.012; const double dTextGap = bHasViewport ? SCALAR_BAR_TEXT_GAP_PIXELS * dPixelX : 0.008; const bool bConstantLabelsVisible = m_pConstantMinimumValueActor->GetVisibility() != 0 && !m_sConstantValueText.isEmpty(); const double dConstantTextHeight = getOverlayTextHeightPixels( m_pConstantMinimumValueActor, pRenderer, SCALAR_BAR_LABEL_FONT_SIZE); const bool bBelowRangeVisible = m_pBelowRangeSwatchActor->GetVisibility() != 0; const bool bAboveRangeVisible = m_pAboveRangeSwatchActor->GetVisibility() != 0; const double dBelowTextHeight = getOverlayTextHeightPixels( m_pBelowRangeTextActor, pRenderer, SCALAR_BAR_LABEL_FONT_SIZE); const double dAboveTextHeight = getOverlayTextHeightPixels( m_pAboveRangeTextActor, pRenderer, SCALAR_BAR_LABEL_FONT_SIZE); const double dBelowClearance = bConstantLabelsVisible && bBelowRangeVisible ? qMax(0.0, (dConstantTextHeight + dBelowTextHeight - SCALAR_BAR_SWATCH_HEIGHT_PIXELS) * 0.5 * dPixelY) : 0.0; const double dAboveClearance = bConstantLabelsVisible && bAboveRangeVisible ? qMax(0.0, (dConstantTextHeight + dAboveTextHeight - SCALAR_BAR_SWATCH_HEIGHT_PIXELS) * 0.5 * dPixelY) : 0.0; // 单值端点与越界说明可以同时启用。将越界注记整体向外错开, // 保证其文字区域不会覆盖位于实际色带端点的单值标签。 const double dBelowY = dColorY - dSwatchGap - dSwatchHeight - dBelowClearance; double dAboveX = dColorX; double dAboveY = dColorY + dColorHeight + dSwatchGap + dAboveClearance; // 标题默认保持原有偏移;Above max 向外避让后若更高,则标题继续上移。 const double dAboveTextHalfHeight = m_pAboveRangeTextActor->GetVisibility() != 0 ? dAboveTextHeight * 0.5 * dPixelY : 0.0; const double dAboveContentHalfHeight = qMax( dSwatchHeight * 0.5, dAboveTextHalfHeight); const double dAboveContentTop = dAboveY + dSwatchHeight * 0.5 + dAboveContentHalfHeight; const double dBaseTitleY = dColorY + dColorHeight + SCALAR_BAR_TITLE_OFFSET_Y_PIXELS * dPixelY; const double dDesiredTitleY = bAboveRangeVisible ? qMax(dBaseTitleY, dAboveContentTop + SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS * dPixelY) : dBaseTitleY; const double dMaximumTitleY = bHasViewport ? qMax(0.0, 1.0 - SCALAR_BAR_TITLE_SAFE_HEIGHT_PIXELS * dPixelY) : dDesiredTitleY; const double dTitleY = qMin(dDesiredTitleY, dMaximumTitleY); if(bAboveRangeVisible && dTitleY < dAboveContentTop + SCALAR_BAR_TITLE_CONTENT_GAP_PIXELS * dPixelY) { // 极小视口无法同时向上容纳标题和 Above max。此时保持标题不越界, // 并把越界注记放到标题、单值标签两者的右侧,消除水平交叠。 const double dConstantTextWidth = bConstantLabelsVisible ? getOverlayTextWidthPixels( m_pConstantMaximumValueActor, pRenderer, m_sConstantValueText.length() * SCALAR_BAR_LABEL_FONT_SIZE * 0.6) : 0.0; const double dTitleWidth = getOverlayTextWidthPixels( m_pTitleActor, pRenderer, m_sTitle.length() * SCALAR_BAR_TITLE_FONT_SIZE * 0.6); dAboveX = qMax( dLabelX + dConstantTextWidth * dPixelX + dTextGap, dColorX + dTitleWidth * dPixelX + dTextGap); dAboveY = dColorY + dColorHeight - dSwatchHeight * 0.5; } m_pTitleActor->GetPositionCoordinate()->SetValue( dColorX, dTitleY, 0.0); updateRectangle(m_pBelowRangeSwatchPoints, dColorX, dBelowY, dSwatchWidth, dSwatchHeight); updateRectangle(m_pAboveRangeSwatchPoints, dAboveX, dAboveY, dSwatchWidth, dSwatchHeight); m_pBelowRangeTextActor->GetPositionCoordinate()->SetValue( dColorX + dSwatchWidth + dTextGap, dBelowY + dSwatchHeight * 0.5, 0.0); m_pAboveRangeTextActor->GetPositionCoordinate()->SetValue( dAboveX + dSwatchWidth + dTextGap, dAboveY + dSwatchHeight * 0.5, 0.0); } void nmVTKScalarBarLayer::updateRangeSwatchColors() { if(m_pLookupTable == nullptr) { return; } // 读取 LookupTable 的业务颜色属性,不直接访问内部特殊表项索引。 const double* pBelowColor = m_pLookupTable->GetBelowRangeColor(); const double* pAboveColor = m_pLookupTable->GetAboveRangeColor(); m_pBelowRangeSwatchActor->GetProperty()->SetColor( pBelowColor[0], pBelowColor[1], pBelowColor[2]); m_pBelowRangeSwatchActor->GetProperty()->SetOpacity(pBelowColor[3]); m_pAboveRangeSwatchActor->GetProperty()->SetColor( pAboveColor[0], pAboveColor[1], pAboveColor[2]); m_pAboveRangeSwatchActor->GetProperty()->SetOpacity(pAboveColor[3]); } vtkScalarBarActor* nmVTKScalarBarLayer::getActor() const { return m_pActor; }